Skip to content

Commit d149f17

Browse files
authored
fix: Write response deserializer for awsJson (#551)
1 parent 5518dd4 commit d149f17

File tree

7 files changed

+261
-27
lines changed

7 files changed

+261
-27
lines changed

Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// SPDX-License-Identifier: Apache-2.0
66
//
77

8+
import struct Foundation.Data
9+
810
extension EventStream {
911
/// Stream adapter that encodes input into `Data` objects.
1012
public class DefaultMessageEncoderStream<Event: MessageMarshallable>: MessageEncoderStream, Stream {

Sources/ClientRuntime/EventStream/Message.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// SPDX-License-Identifier: Apache-2.0
66
//
77

8+
import struct Foundation.Data
9+
810
extension EventStream {
911

1012
/// A message in an event stream that can be sent or received.

Sources/ClientRuntime/Networking/Streaming/Stream.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// SPDX-License-Identifier: Apache-2.0
66
//
77

8+
import struct Foundation.Data
9+
810
import AwsCommonRuntimeKit
911

1012
/// Protocol that provides reading data from a stream

Sources/SmithyTestUtil/XMLComparator.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,10 @@ extension XMLConverter: XMLParserDelegate {
6060
qualifiedName qName: String?,
6161
attributes attributeDict: [String : String] = [:]
6262
) {
63-
let parent = stack.last!
6463
let element = XMLElement(
6564
name: elementName,
6665
attributes: attributeDict
6766
)
68-
6967
stack.append(element)
7068
}
7169

smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithoutHttpPayload.kt

Lines changed: 80 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,26 @@
55

66
package software.amazon.smithy.swift.codegen.integration.httpResponse.bindingTraits
77

8+
import software.amazon.smithy.codegen.core.CodegenException
89
import software.amazon.smithy.model.knowledge.HttpBinding
910
import software.amazon.smithy.model.shapes.BooleanShape
1011
import software.amazon.smithy.model.shapes.ByteShape
1112
import software.amazon.smithy.model.shapes.DoubleShape
1213
import software.amazon.smithy.model.shapes.FloatShape
1314
import software.amazon.smithy.model.shapes.IntegerShape
1415
import software.amazon.smithy.model.shapes.LongShape
16+
import software.amazon.smithy.model.shapes.ShapeType
1517
import software.amazon.smithy.model.shapes.ShortShape
1618
import software.amazon.smithy.model.traits.HttpQueryTrait
19+
import software.amazon.smithy.model.traits.StreamingTrait
20+
import software.amazon.smithy.swift.codegen.ClientRuntimeTypes
1721
import software.amazon.smithy.swift.codegen.SwiftWriter
22+
import software.amazon.smithy.swift.codegen.declareSection
1823
import software.amazon.smithy.swift.codegen.integration.HttpBindingDescriptor
1924
import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator
2025
import software.amazon.smithy.swift.codegen.integration.httpResponse.HttpResponseBindingRenderable
2126
import software.amazon.smithy.swift.codegen.model.isBoxed
27+
import software.amazon.smithy.swift.codegen.model.targetOrSelf
2228

2329
interface HttpResponseTraitWithoutHttpPayloadFactory {
2430
fun construct(
@@ -37,39 +43,88 @@ class HttpResponseTraitWithoutHttpPayload(
3743
) : HttpResponseBindingRenderable {
3844
override fun render() {
3945
val bodyMembers = responseBindings.filter { it.location == HttpBinding.Location.DOCUMENT }
40-
4146
val bodyMembersWithoutQueryTrait = bodyMembers
4247
.filter { !it.member.hasTrait(HttpQueryTrait::class.java) }
4348
.toMutableSet()
49+
val streamingMember = bodyMembers.firstOrNull { it.member.targetOrSelf(ctx.model).hasTrait(StreamingTrait::class.java) }
4450

45-
val bodyMembersWithoutQueryTraitMemberNames = bodyMembersWithoutQueryTrait.map { ctx.symbolProvider.toMemberName(it.member) }
51+
if (streamingMember != null) {
52+
writeStreamingMember(streamingMember)
53+
} else if (bodyMembersWithoutQueryTrait.isNotEmpty()) {
54+
writeNonStreamingMembers(bodyMembersWithoutQueryTrait)
55+
}
56+
}
4657

47-
if (bodyMembersWithoutQueryTrait.isNotEmpty()) {
48-
writer.write("if let data = try httpResponse.body.toData(),")
49-
writer.indent()
50-
writer.write("let responseDecoder = decoder {")
51-
writer.write("let output: ${outputShapeName}Body = try responseDecoder.decode(responseBody: data)")
52-
bodyMembersWithoutQueryTraitMemberNames.sorted().forEach {
53-
writer.write("self.$it = output.$it")
54-
}
55-
writer.dedent()
56-
writer.write("} else {")
57-
writer.indent()
58-
bodyMembersWithoutQueryTrait.sortedBy { it.memberName }.forEach {
59-
val memberName = ctx.symbolProvider.toMemberName(it.member)
60-
val type = ctx.model.expectShape(it.member.target)
61-
val value = if (ctx.symbolProvider.toSymbol(it.member).isBoxed()) "nil" else {
62-
when (type) {
63-
is IntegerShape, is ByteShape, is ShortShape, is LongShape -> 0
64-
is FloatShape, is DoubleShape -> 0.0
65-
is BooleanShape -> false
66-
else -> "nil"
58+
fun writeStreamingMember(streamingMember: HttpBindingDescriptor) {
59+
val shape = ctx.model.expectShape(streamingMember.member.target)
60+
val symbol = ctx.symbolProvider.toSymbol(shape)
61+
val memberName = ctx.symbolProvider.toMemberName(streamingMember.member)
62+
when (shape.type) {
63+
ShapeType.UNION -> {
64+
writer.openBlock("if case let .stream(stream) = httpResponse.body, let responseDecoder = decoder {", "} else {") {
65+
writer.declareSection(HttpResponseTraitWithHttpPayload.MessageDecoderSectionId) {
66+
writer.write("let messageDecoder: \$D", ClientRuntimeTypes.EventStream.MessageDecoder)
6767
}
68+
writer.write(
69+
"let decoderStream = \$L<\$N>(stream: stream, messageDecoder: messageDecoder, responseDecoder: responseDecoder)",
70+
ClientRuntimeTypes.EventStream.MessageDecoderStream,
71+
symbol
72+
)
73+
writer.write("self.\$L = decoderStream.toAsyncStream()", memberName)
74+
}
75+
writer.indent()
76+
writer.write("self.\$L = nil", memberName).closeBlock("}")
77+
}
78+
ShapeType.BLOB -> {
79+
writer.write("switch httpResponse.body {")
80+
.write("case .data(let data):")
81+
.indent()
82+
writer.write("self.\$L = .data(data)", memberName)
83+
84+
// For binary streams, we need to set the member to the stream directly.
85+
// this allows us to stream the data directly to the user
86+
// without having to buffer it in memory.
87+
writer.dedent()
88+
.write("case .stream(let stream):")
89+
.indent()
90+
writer.write("self.\$L = .stream(stream)", memberName)
91+
writer.dedent()
92+
.write("case .none:")
93+
.indent()
94+
.write("self.\$L = nil", memberName).closeBlock("}")
95+
}
96+
else -> {
97+
throw CodegenException("member shape ${streamingMember.member} cannot stream")
98+
}
99+
}
100+
}
101+
102+
fun writeNonStreamingMembers(members: Set<HttpBindingDescriptor>) {
103+
val memberNames = members.map { ctx.symbolProvider.toMemberName(it.member) }
104+
writer.write("if let data = try httpResponse.body.toData(),")
105+
writer.indent()
106+
writer.write("let responseDecoder = decoder {")
107+
writer.write("let output: ${outputShapeName}Body = try responseDecoder.decode(responseBody: data)")
108+
memberNames.sorted().forEach {
109+
writer.write("self.$it = output.$it")
110+
}
111+
writer.dedent()
112+
writer.write("} else {")
113+
writer.indent()
114+
members.sortedBy { it.memberName }.forEach {
115+
val memberName = ctx.symbolProvider.toMemberName(it.member)
116+
val type = ctx.model.expectShape(it.member.target)
117+
val value = if (ctx.symbolProvider.toSymbol(it.member).isBoxed()) "nil" else {
118+
when (type) {
119+
is IntegerShape, is ByteShape, is ShortShape, is LongShape -> 0
120+
is FloatShape, is DoubleShape -> 0.0
121+
is BooleanShape -> false
122+
else -> "nil"
68123
}
69-
writer.write("self.$memberName = $value")
70124
}
71-
writer.dedent()
72-
writer.write("}")
125+
writer.write("self.$memberName = $value")
73126
}
127+
writer.dedent()
128+
writer.write("}")
74129
}
75130
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package serde.awsjson11
2+
3+
import TestContext
4+
import asSmithy
5+
import defaultSettings
6+
import getModelFileContents
7+
import io.kotest.matchers.string.shouldContainOnlyOnce
8+
import newTestContext
9+
import org.junit.jupiter.api.Test
10+
import shouldSyntacticSanityCheck
11+
import software.amazon.smithy.swift.codegen.model.AddOperationShapes
12+
13+
// NOTE: protocol conformance is mostly handled by the protocol tests suite
14+
class OutputResponseDeserializerTests {
15+
private var model = javaClass.getResource("awsjson-output-response-deserializer.smithy").asSmithy()
16+
private fun newTestContext(): TestContext {
17+
val settings = model.defaultSettings()
18+
model = AddOperationShapes.execute(model, settings.getService(model), settings.moduleName)
19+
return model.newTestContext()
20+
}
21+
22+
val newTestContext = newTestContext()
23+
24+
init {
25+
newTestContext.generator.generateSerializers(newTestContext.generationCtx)
26+
newTestContext.generator.generateProtocolClient(newTestContext.generationCtx)
27+
newTestContext.generator.generateDeserializers(newTestContext.generationCtx)
28+
newTestContext.generator.generateCodableConformanceForNestedTypes(newTestContext.generationCtx)
29+
newTestContext.generationCtx.delegator.flushWriters()
30+
}
31+
32+
@Test
33+
fun `it creates correct init for simple structure payloads`() {
34+
val contents = getModelFileContents(
35+
"example",
36+
"SimpleStructureOutputResponse+HttpResponseBinding.swift",
37+
newTestContext.manifest
38+
)
39+
contents.shouldSyntacticSanityCheck()
40+
val expectedContents =
41+
"""
42+
extension SimpleStructureOutputResponse: ClientRuntime.HttpResponseBinding {
43+
public init (httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder? = nil) throws {
44+
if let data = try httpResponse.body.toData(),
45+
let responseDecoder = decoder {
46+
let output: SimpleStructureOutputResponseBody = try responseDecoder.decode(responseBody: data)
47+
self.name = output.name
48+
self.number = output.number
49+
} else {
50+
self.name = nil
51+
self.number = nil
52+
}
53+
}
54+
}
55+
""".trimIndent()
56+
contents.shouldContainOnlyOnce(expectedContents)
57+
}
58+
59+
@Test
60+
fun `it creates correct init for data streaming payloads`() {
61+
val contents = getModelFileContents(
62+
"example",
63+
"DataStreamingOutputResponse+HttpResponseBinding.swift",
64+
newTestContext.manifest
65+
)
66+
contents.shouldSyntacticSanityCheck()
67+
val expectedContents =
68+
"""
69+
extension DataStreamingOutputResponse: ClientRuntime.HttpResponseBinding {
70+
public init (httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder? = nil) throws {
71+
switch httpResponse.body {
72+
case .data(let data):
73+
self.streamingData = .data(data)
74+
case .stream(let stream):
75+
self.streamingData = .stream(stream)
76+
case .none:
77+
self.streamingData = nil
78+
}
79+
}
80+
}
81+
""".trimIndent()
82+
contents.shouldContainOnlyOnce(expectedContents)
83+
}
84+
85+
@Test
86+
fun `it creates correct init for event streaming payloads`() {
87+
val contents = getModelFileContents(
88+
"example",
89+
"EventStreamingOutputResponse+HttpResponseBinding.swift",
90+
newTestContext.manifest
91+
)
92+
contents.shouldSyntacticSanityCheck()
93+
val expectedContents =
94+
"""
95+
extension EventStreamingOutputResponse: ClientRuntime.HttpResponseBinding {
96+
public init (httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder? = nil) throws {
97+
if case let .stream(stream) = httpResponse.body, let responseDecoder = decoder {
98+
let messageDecoder: ClientRuntime.MessageDecoder? = nil
99+
let decoderStream = ClientRuntime.EventStream.DefaultMessageDecoderStream<EventStream>(stream: stream, messageDecoder: messageDecoder, responseDecoder: responseDecoder)
100+
self.eventStream = decoderStream.toAsyncStream()
101+
} else {
102+
self.eventStream = nil
103+
}
104+
}
105+
}
106+
""".trimIndent()
107+
contents.shouldContainOnlyOnce(expectedContents)
108+
}
109+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
$version: "2.0"
2+
namespace com.test
3+
4+
use aws.protocols#awsJson1_1
5+
6+
@awsJson1_1
7+
service Example {
8+
version: "1.0.0",
9+
operations: [
10+
SimpleStructure,
11+
DataStreaming,
12+
EventStreaming
13+
]
14+
}
15+
16+
@http(method: "PUT", uri: "/SimpleStructure")
17+
operation SimpleStructure {
18+
input: Input
19+
output: SimpleStructureOutput
20+
}
21+
22+
structure Input {}
23+
24+
structure SimpleStructureOutput {
25+
name: Name
26+
number: Number
27+
}
28+
29+
string Name
30+
31+
integer Number
32+
33+
@http(method: "PUT", uri: "/DataStreaming")
34+
operation DataStreaming {
35+
input: Input
36+
output: DataStreamingOutput
37+
}
38+
39+
structure DataStreamingOutput {
40+
@required
41+
streamingData: StreamingData
42+
}
43+
44+
@streaming
45+
blob StreamingData
46+
47+
@http(method: "PUT", uri: "/EventStreaming")
48+
operation EventStreaming {
49+
input: Input
50+
output: EventStreamingOutput
51+
}
52+
53+
structure EventStreamingOutput {
54+
@required
55+
eventStream: EventStream
56+
}
57+
58+
@streaming
59+
union EventStream {
60+
eventA: EventA
61+
eventB: EventB
62+
}
63+
64+
structure EventA {}
65+
66+
structure EventB {}

0 commit comments

Comments
 (0)