Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support initial-response in RPC based event streams #597

Merged
merged 24 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b2f5023
add initial messages support
dayaffe Sep 13, 2023
c72f0cc
remove unnecessary space
dayaffe Sep 14, 2023
d7f0dc3
Merge branch 'main' into day/initial-messages
dayaffe Sep 14, 2023
02c0a83
add tests + general function in helper file
dayaffe Sep 14, 2023
200781a
rename initialData
dayaffe Sep 14, 2023
8605a5b
reorder imports
dayaffe Sep 18, 2023
db2baf5
support initial-request and add tests
dayaffe Sep 20, 2023
ebb7616
lint
dayaffe Sep 20, 2023
c73c089
Merge branch 'main' into day/initial-messages
dayaffe Sep 20, 2023
988b5db
Merge branch 'main' into day/initial-messages
dayaffe Sep 20, 2023
49b2e73
fix swift codegen to properly parse initial request members
dayaffe Sep 21, 2023
6ce978d
Merge branch 'main' into day/initial-messages
dayaffe Sep 21, 2023
d837a55
remove initial-request support
dayaffe Oct 3, 2023
664b86f
remove initial-request support + add initial-response codegen test
dayaffe Oct 3, 2023
fb5e28d
Merge branch 'main' into day/initial-response-rpc-support
dayaffe Oct 3, 2023
f55e2f0
remove unnecessary imports
dayaffe Oct 3, 2023
7eceb6d
add test files
dayaffe Oct 3, 2023
bcfaded
lint
dayaffe Oct 3, 2023
a8f5e5e
initialResponse code should only apply when members are present + RPC
dayaffe Oct 4, 2023
19375bc
use Set and utility writers
dayaffe Oct 5, 2023
3b56952
revert use of open and close block
dayaffe Oct 5, 2023
e8048fd
Merge branch 'main' into day/initial-response-rpc-support
dayaffe Oct 11, 2023
182a6b2
only need to calculate initialResponseMembers if there is a streaming…
dayaffe Oct 11, 2023
605f69a
fix failing test
dayaffe Oct 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,19 @@ class HttpResponseTraitWithoutHttpPayload(
val bodyMembersWithoutQueryTrait = bodyMembers
.filter { !it.member.hasTrait(HttpQueryTrait::class.java) }
.toMutableSet()
val initialResponseMembers = bodyMembers.filter {
val targetShape = it.member.targetOrSelf(ctx.model)
targetShape?.hasTrait(StreamingTrait::class.java) == false
jbelkins marked this conversation as resolved.
Show resolved Hide resolved
}.toSet()
val streamingMember = bodyMembers.firstOrNull { it.member.targetOrSelf(ctx.model).hasTrait(StreamingTrait::class.java) }

if (streamingMember != null) {
writeStreamingMember(streamingMember)
writeStreamingMember(streamingMember, initialResponseMembers)
} else if (bodyMembersWithoutQueryTrait.isNotEmpty()) {
writeNonStreamingMembers(bodyMembersWithoutQueryTrait)
}
}

fun writeStreamingMember(streamingMember: HttpBindingDescriptor) {
fun writeStreamingMember(streamingMember: HttpBindingDescriptor, initialResponseMembers: Set<HttpBindingDescriptor>) {
val shape = ctx.model.expectShape(streamingMember.member.target)
val symbol = ctx.symbolProvider.toSymbol(shape)
val memberName = ctx.symbolProvider.toMemberName(streamingMember.member)
Expand All @@ -74,6 +77,9 @@ class HttpResponseTraitWithoutHttpPayload(
symbol
)
writer.write("self.\$L = decoderStream.toAsyncStream()", memberName)
if (isRPCService(ctx) && initialResponseMembers.isNotEmpty()) {
writeInitialResponseMembers(initialResponseMembers)
}
}
writer.indent()
writer.write("self.\$L = nil", memberName).closeBlock("}")
Expand Down Expand Up @@ -133,4 +139,52 @@ class HttpResponseTraitWithoutHttpPayload(
}

private val path: String = "properties.".takeIf { outputShape.hasTrait<ErrorTrait>() } ?: ""

private fun writeInitialResponseMembers(initialResponseMembers: Set<HttpBindingDescriptor>) {
writer.apply {
write("if let initialDataWithoutHttp = await messageDecoder.awaitInitialResponse() {")
indent()
write("let decoder = JSONDecoder()")
write("do {")
indent()
write("let response = try decoder.decode([String: String].self, from: initialDataWithoutHttp)")
initialResponseMembers.forEach { responseMember ->
val responseMemberName = ctx.symbolProvider.toMemberName(responseMember.member)
write("self.$responseMemberName = response[\"$responseMemberName\"].map { value in KinesisClientTypes.Tag(value: value) }")
}
dedent()
write("} catch {")
indent()
write("print(\"Error decoding JSON: \\(error)\")")
initialResponseMembers.forEach { responseMember ->
val responseMemberName = ctx.symbolProvider.toMemberName(responseMember.member)
write("self.$responseMemberName = nil")
}
dedent()
write("}")
dedent()
write("} else {")
indent()
initialResponseMembers.forEach { responseMember ->
val responseMemberName = ctx.symbolProvider.toMemberName(responseMember.member)
write("self.$responseMemberName = nil")
}
dedent()
write("}")
}
}

private fun isRPCService(ctx: ProtocolGenerator.GenerationContext): Boolean {
return rpcBoundProtocols.contains(ctx.protocol.name)
}

/**
* A set of RPC-bound Smithy protocols
*/
private val rpcBoundProtocols = setOf(
"awsJson1_0",
"awsJson1_1",
"awsQuery",
"ec2Query",
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

import io.kotest.matchers.string.shouldContainOnlyOnce
import mocks.MockHttpAWSJson11ProtocolGenerator
import org.junit.jupiter.api.Test
import software.amazon.smithy.swift.codegen.integration.HttpBindingProtocolGenerator

class EventStreamsInitialResponseTests {
@Test
fun `should attempt to decode response if initial-response members are present in RPC (awsJson) smithy model`() {
val context = setupInitialMessageTests(
"event-stream-initial-request-response.smithy",
"com.test#Example",
MockHttpAWSJson11ProtocolGenerator()
)
val contents = getFileContents(
context.manifest,
"/InitialMessageEventStreams/models/TestStreamOperationWithInitialRequestResponseOutputResponse+HttpResponseBinding.swift"
)
contents.shouldSyntacticSanityCheck()
contents.shouldContainOnlyOnce(
"""
extension TestStreamOperationWithInitialRequestResponseOutputResponse: ClientRuntime.HttpResponseBinding {
public init(httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder? = nil) async throws {
if case let .stream(stream) = httpResponse.body, let responseDecoder = decoder {
let messageDecoder: ClientRuntime.MessageDecoder? = nil
let decoderStream = ClientRuntime.EventStream.DefaultMessageDecoderStream<InitialMessageEventStreamsClientTypes.TestStream>(stream: stream, messageDecoder: messageDecoder, responseDecoder: responseDecoder)
self.value = decoderStream.toAsyncStream()
if let initialDataWithoutHttp = await messageDecoder.awaitInitialResponse() {
let decoder = JSONDecoder()
do {
let response = try decoder.decode([String: String].self, from: initialDataWithoutHttp)
self.initial1 = response["initial1"].map { value in KinesisClientTypes.Tag(value: value) }
self.initial2 = response["initial2"].map { value in KinesisClientTypes.Tag(value: value) }
} catch {
print("Error decoding JSON: \(error)")
self.initial1 = nil
self.initial2 = nil
}
} else {
self.initial1 = nil
self.initial2 = nil
}
} else {
self.value = nil
}
}
}
""".trimIndent()
)
}

private fun setupInitialMessageTests(
smithyFile: String,
serviceShapeId: String,
protocolGenerator: HttpBindingProtocolGenerator
): TestContext {
val context = TestContext.initContextFrom(smithyFile, serviceShapeId, protocolGenerator) { model ->
model.defaultSettings(serviceShapeId, "InitialMessageEventStreams", "123", "InitialMessageEventStreams")
}
context.generator.initializeMiddleware(context.generationCtx)
context.generator.generateSerializers(context.generationCtx)
context.generator.generateProtocolClient(context.generationCtx)
context.generator.generateDeserializers(context.generationCtx)
context.generator.generateCodableConformanceForNestedTypes(context.generationCtx)
context.generationCtx.delegator.flushWriters()
return context
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class MockAWSJson11HttpProtocolCustomizations() : DefaultHttpProtocolCustomizati
writer: SwiftWriter,
op: OperationShape,
) {
TODO("Not yet implemented")
// Not yet implemented
return
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace com.test

use aws.protocols#awsJson1_1
use aws.api#service
use aws.auth#sigv4

@awsJson1_1
@sigv4(name: "event-stream-test")
@service(sdkId: "InitialMessageEventStreams")
service Example {
version: "123",
operations: [TestStreamOperationWithInitialRequestResponse]
}

operation TestStreamOperationWithInitialRequestResponse {
input: TestStreamInputOutputInitialRequestResponse,
output: TestStreamInputOutputInitialRequestResponse,
errors: [SomeError],
}

structure TestStreamInputOutputInitialRequestResponse {
@required
value: TestStream
initial1: String
initial2: String
}

@error("client")
structure SomeError {
Message: String,
}

structure MessageWithString { @eventPayload data: String }

@streaming
union TestStream {
MessageWithString: MessageWithString,
SomeError: SomeError,
}
Loading