Skip to content

Commit

Permalink
fix: Write response deserializer for awsJson (#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbelkins committed May 3, 2023
1 parent 5518dd4 commit d149f17
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Event: MessageMarshallable>: MessageEncoderStream, Stream {
Expand Down
2 changes: 2 additions & 0 deletions Sources/ClientRuntime/EventStream/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions Sources/ClientRuntime/Networking/Streaming/Stream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
// SPDX-License-Identifier: Apache-2.0
//

import struct Foundation.Data

import AwsCommonRuntimeKit

/// Protocol that provides reading data from a stream
Expand Down
2 changes: 0 additions & 2 deletions Sources/SmithyTestUtil/XMLComparator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@

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
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(
Expand All @@ -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<HttpBindingDescriptor>) {
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("}")
}
}
Original file line number Diff line number Diff line change
@@ -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<EventStream>(stream: stream, messageDecoder: messageDecoder, responseDecoder: responseDecoder)
self.eventStream = decoderStream.toAsyncStream()
} else {
self.eventStream = nil
}
}
}
""".trimIndent()
contents.shouldContainOnlyOnce(expectedContents)
}
}
Original file line number Diff line number Diff line change
@@ -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 {}

0 comments on commit d149f17

Please sign in to comment.