diff --git a/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift b/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift index ef0d41d81..5b93b2603 100644 --- a/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift +++ b/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift @@ -5,6 +5,8 @@ // SPDX-License-Identifier: Apache-2.0 // +import struct Foundation.Data + extension EventStream { /// Stream adapter that encodes input into `Data` objects. public class DefaultMessageEncoderStream: MessageEncoderStream, Stream { diff --git a/Sources/ClientRuntime/EventStream/Message.swift b/Sources/ClientRuntime/EventStream/Message.swift index 7f4aee8b2..e85e69e1b 100644 --- a/Sources/ClientRuntime/EventStream/Message.swift +++ b/Sources/ClientRuntime/EventStream/Message.swift @@ -5,6 +5,8 @@ // SPDX-License-Identifier: Apache-2.0 // +import struct Foundation.Data + extension EventStream { /// A message in an event stream that can be sent or received. diff --git a/Sources/ClientRuntime/Networking/Streaming/Stream.swift b/Sources/ClientRuntime/Networking/Streaming/Stream.swift index 9244b5edb..f1c091317 100644 --- a/Sources/ClientRuntime/Networking/Streaming/Stream.swift +++ b/Sources/ClientRuntime/Networking/Streaming/Stream.swift @@ -5,6 +5,8 @@ // SPDX-License-Identifier: Apache-2.0 // +import struct Foundation.Data + import AwsCommonRuntimeKit /// Protocol that provides reading data from a stream diff --git a/Sources/SmithyTestUtil/XMLComparator.swift b/Sources/SmithyTestUtil/XMLComparator.swift index 42f08b8d8..f396101dc 100644 --- a/Sources/SmithyTestUtil/XMLComparator.swift +++ b/Sources/SmithyTestUtil/XMLComparator.swift @@ -60,12 +60,10 @@ extension XMLConverter: XMLParserDelegate { qualifiedName qName: String?, attributes attributeDict: [String : String] = [:] ) { - let parent = stack.last! let element = XMLElement( name: elementName, attributes: attributeDict ) - stack.append(element) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithoutHttpPayload.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithoutHttpPayload.kt index 39cb90d68..32bf26cf2 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithoutHttpPayload.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithoutHttpPayload.kt @@ -5,6 +5,7 @@ package software.amazon.smithy.swift.codegen.integration.httpResponse.bindingTraits +import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.model.knowledge.HttpBinding import software.amazon.smithy.model.shapes.BooleanShape import software.amazon.smithy.model.shapes.ByteShape @@ -12,13 +13,18 @@ import software.amazon.smithy.model.shapes.DoubleShape import software.amazon.smithy.model.shapes.FloatShape import software.amazon.smithy.model.shapes.IntegerShape import software.amazon.smithy.model.shapes.LongShape +import software.amazon.smithy.model.shapes.ShapeType import software.amazon.smithy.model.shapes.ShortShape import software.amazon.smithy.model.traits.HttpQueryTrait +import software.amazon.smithy.model.traits.StreamingTrait +import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.declareSection import software.amazon.smithy.swift.codegen.integration.HttpBindingDescriptor import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.httpResponse.HttpResponseBindingRenderable import software.amazon.smithy.swift.codegen.model.isBoxed +import software.amazon.smithy.swift.codegen.model.targetOrSelf interface HttpResponseTraitWithoutHttpPayloadFactory { fun construct( @@ -37,39 +43,88 @@ class HttpResponseTraitWithoutHttpPayload( ) : HttpResponseBindingRenderable { override fun render() { val bodyMembers = responseBindings.filter { it.location == HttpBinding.Location.DOCUMENT } - val bodyMembersWithoutQueryTrait = bodyMembers .filter { !it.member.hasTrait(HttpQueryTrait::class.java) } .toMutableSet() + val streamingMember = bodyMembers.firstOrNull { it.member.targetOrSelf(ctx.model).hasTrait(StreamingTrait::class.java) } - val bodyMembersWithoutQueryTraitMemberNames = bodyMembersWithoutQueryTrait.map { ctx.symbolProvider.toMemberName(it.member) } + if (streamingMember != null) { + writeStreamingMember(streamingMember) + } else if (bodyMembersWithoutQueryTrait.isNotEmpty()) { + writeNonStreamingMembers(bodyMembersWithoutQueryTrait) + } + } - if (bodyMembersWithoutQueryTrait.isNotEmpty()) { - writer.write("if let data = try httpResponse.body.toData(),") - writer.indent() - writer.write("let responseDecoder = decoder {") - writer.write("let output: ${outputShapeName}Body = try responseDecoder.decode(responseBody: data)") - bodyMembersWithoutQueryTraitMemberNames.sorted().forEach { - writer.write("self.$it = output.$it") - } - writer.dedent() - writer.write("} else {") - writer.indent() - bodyMembersWithoutQueryTrait.sortedBy { it.memberName }.forEach { - val memberName = ctx.symbolProvider.toMemberName(it.member) - val type = ctx.model.expectShape(it.member.target) - val value = if (ctx.symbolProvider.toSymbol(it.member).isBoxed()) "nil" else { - when (type) { - is IntegerShape, is ByteShape, is ShortShape, is LongShape -> 0 - is FloatShape, is DoubleShape -> 0.0 - is BooleanShape -> false - else -> "nil" + fun writeStreamingMember(streamingMember: HttpBindingDescriptor) { + val shape = ctx.model.expectShape(streamingMember.member.target) + val symbol = ctx.symbolProvider.toSymbol(shape) + val memberName = ctx.symbolProvider.toMemberName(streamingMember.member) + when (shape.type) { + ShapeType.UNION -> { + writer.openBlock("if case let .stream(stream) = httpResponse.body, let responseDecoder = decoder {", "} else {") { + writer.declareSection(HttpResponseTraitWithHttpPayload.MessageDecoderSectionId) { + writer.write("let messageDecoder: \$D", ClientRuntimeTypes.EventStream.MessageDecoder) } + writer.write( + "let decoderStream = \$L<\$N>(stream: stream, messageDecoder: messageDecoder, responseDecoder: responseDecoder)", + ClientRuntimeTypes.EventStream.MessageDecoderStream, + symbol + ) + writer.write("self.\$L = decoderStream.toAsyncStream()", memberName) + } + writer.indent() + writer.write("self.\$L = nil", memberName).closeBlock("}") + } + ShapeType.BLOB -> { + writer.write("switch httpResponse.body {") + .write("case .data(let data):") + .indent() + writer.write("self.\$L = .data(data)", memberName) + + // For binary streams, we need to set the member to the stream directly. + // this allows us to stream the data directly to the user + // without having to buffer it in memory. + writer.dedent() + .write("case .stream(let stream):") + .indent() + writer.write("self.\$L = .stream(stream)", memberName) + writer.dedent() + .write("case .none:") + .indent() + .write("self.\$L = nil", memberName).closeBlock("}") + } + else -> { + throw CodegenException("member shape ${streamingMember.member} cannot stream") + } + } + } + + fun writeNonStreamingMembers(members: Set) { + val memberNames = members.map { ctx.symbolProvider.toMemberName(it.member) } + writer.write("if let data = try httpResponse.body.toData(),") + writer.indent() + writer.write("let responseDecoder = decoder {") + writer.write("let output: ${outputShapeName}Body = try responseDecoder.decode(responseBody: data)") + memberNames.sorted().forEach { + writer.write("self.$it = output.$it") + } + writer.dedent() + writer.write("} else {") + writer.indent() + members.sortedBy { it.memberName }.forEach { + val memberName = ctx.symbolProvider.toMemberName(it.member) + val type = ctx.model.expectShape(it.member.target) + val value = if (ctx.symbolProvider.toSymbol(it.member).isBoxed()) "nil" else { + when (type) { + is IntegerShape, is ByteShape, is ShortShape, is LongShape -> 0 + is FloatShape, is DoubleShape -> 0.0 + is BooleanShape -> false + else -> "nil" } - writer.write("self.$memberName = $value") } - writer.dedent() - writer.write("}") + writer.write("self.$memberName = $value") } + writer.dedent() + writer.write("}") } } diff --git a/smithy-swift-codegen/src/test/kotlin/serde/awsjson11/OutputResponseDeserializerTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/awsjson11/OutputResponseDeserializerTests.kt new file mode 100644 index 000000000..64f03b7f2 --- /dev/null +++ b/smithy-swift-codegen/src/test/kotlin/serde/awsjson11/OutputResponseDeserializerTests.kt @@ -0,0 +1,109 @@ +package serde.awsjson11 + +import TestContext +import asSmithy +import defaultSettings +import getModelFileContents +import io.kotest.matchers.string.shouldContainOnlyOnce +import newTestContext +import org.junit.jupiter.api.Test +import shouldSyntacticSanityCheck +import software.amazon.smithy.swift.codegen.model.AddOperationShapes + +// NOTE: protocol conformance is mostly handled by the protocol tests suite +class OutputResponseDeserializerTests { + private var model = javaClass.getResource("awsjson-output-response-deserializer.smithy").asSmithy() + private fun newTestContext(): TestContext { + val settings = model.defaultSettings() + model = AddOperationShapes.execute(model, settings.getService(model), settings.moduleName) + return model.newTestContext() + } + + val newTestContext = newTestContext() + + init { + newTestContext.generator.generateSerializers(newTestContext.generationCtx) + newTestContext.generator.generateProtocolClient(newTestContext.generationCtx) + newTestContext.generator.generateDeserializers(newTestContext.generationCtx) + newTestContext.generator.generateCodableConformanceForNestedTypes(newTestContext.generationCtx) + newTestContext.generationCtx.delegator.flushWriters() + } + + @Test + fun `it creates correct init for simple structure payloads`() { + val contents = getModelFileContents( + "example", + "SimpleStructureOutputResponse+HttpResponseBinding.swift", + newTestContext.manifest + ) + contents.shouldSyntacticSanityCheck() + val expectedContents = + """ + extension SimpleStructureOutputResponse: ClientRuntime.HttpResponseBinding { + public init (httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder? = nil) throws { + if let data = try httpResponse.body.toData(), + let responseDecoder = decoder { + let output: SimpleStructureOutputResponseBody = try responseDecoder.decode(responseBody: data) + self.name = output.name + self.number = output.number + } else { + self.name = nil + self.number = nil + } + } + } + """.trimIndent() + contents.shouldContainOnlyOnce(expectedContents) + } + + @Test + fun `it creates correct init for data streaming payloads`() { + val contents = getModelFileContents( + "example", + "DataStreamingOutputResponse+HttpResponseBinding.swift", + newTestContext.manifest + ) + contents.shouldSyntacticSanityCheck() + val expectedContents = + """ + extension DataStreamingOutputResponse: ClientRuntime.HttpResponseBinding { + public init (httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder? = nil) throws { + switch httpResponse.body { + case .data(let data): + self.streamingData = .data(data) + case .stream(let stream): + self.streamingData = .stream(stream) + case .none: + self.streamingData = nil + } + } + } + """.trimIndent() + contents.shouldContainOnlyOnce(expectedContents) + } + + @Test + fun `it creates correct init for event streaming payloads`() { + val contents = getModelFileContents( + "example", + "EventStreamingOutputResponse+HttpResponseBinding.swift", + newTestContext.manifest + ) + contents.shouldSyntacticSanityCheck() + val expectedContents = + """ + extension EventStreamingOutputResponse: ClientRuntime.HttpResponseBinding { + public init (httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder? = nil) throws { + if case let .stream(stream) = httpResponse.body, let responseDecoder = decoder { + let messageDecoder: ClientRuntime.MessageDecoder? = nil + let decoderStream = ClientRuntime.EventStream.DefaultMessageDecoderStream(stream: stream, messageDecoder: messageDecoder, responseDecoder: responseDecoder) + self.eventStream = decoderStream.toAsyncStream() + } else { + self.eventStream = nil + } + } + } + """.trimIndent() + contents.shouldContainOnlyOnce(expectedContents) + } +} diff --git a/smithy-swift-codegen/src/test/resources/serde/awsjson11/awsjson-output-response-deserializer.smithy b/smithy-swift-codegen/src/test/resources/serde/awsjson11/awsjson-output-response-deserializer.smithy new file mode 100644 index 000000000..1f5e65128 --- /dev/null +++ b/smithy-swift-codegen/src/test/resources/serde/awsjson11/awsjson-output-response-deserializer.smithy @@ -0,0 +1,66 @@ +$version: "2.0" +namespace com.test + +use aws.protocols#awsJson1_1 + +@awsJson1_1 +service Example { + version: "1.0.0", + operations: [ + SimpleStructure, + DataStreaming, + EventStreaming + ] +} + +@http(method: "PUT", uri: "/SimpleStructure") +operation SimpleStructure { + input: Input + output: SimpleStructureOutput +} + +structure Input {} + +structure SimpleStructureOutput { + name: Name + number: Number +} + +string Name + +integer Number + +@http(method: "PUT", uri: "/DataStreaming") +operation DataStreaming { + input: Input + output: DataStreamingOutput +} + +structure DataStreamingOutput { + @required + streamingData: StreamingData +} + +@streaming +blob StreamingData + +@http(method: "PUT", uri: "/EventStreaming") +operation EventStreaming { + input: Input + output: EventStreamingOutput +} + +structure EventStreamingOutput { + @required + eventStream: EventStream +} + +@streaming +union EventStream { + eventA: EventA + eventB: EventB +} + +structure EventA {} + +structure EventB {} \ No newline at end of file