From 81a50434eb0de6dd67360b14f2af9be7d1fd61eb Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 10 Oct 2023 21:55:35 -0500 Subject: [PATCH 01/37] chore: Require Swift 5.7, fix deprecation warnings (#600) --- Package.swift | 6 +++--- .../Retries/DefaultRetryStrategy/RetryQuota.swift | 2 +- Sources/ClientRuntime/Util/SwiftVersion.swift | 14 -------------- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/Package.swift b/Package.swift index 1f5c6ead8..e4ed89dd5 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.7 import PackageDescription @@ -13,9 +13,9 @@ let package = Package( .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]) ], dependencies: [ - .package(url: "https://github.com/awslabs/aws-crt-swift.git", .exact("0.13.0")), + .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.13.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), - .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", .exact("0.17.0")) + .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", exact: "0.17.0") ], targets: [ .target( diff --git a/Sources/ClientRuntime/Retries/DefaultRetryStrategy/RetryQuota.swift b/Sources/ClientRuntime/Retries/DefaultRetryStrategy/RetryQuota.swift index 717783dd2..d516d658c 100644 --- a/Sources/ClientRuntime/Retries/DefaultRetryStrategy/RetryQuota.swift +++ b/Sources/ClientRuntime/Retries/DefaultRetryStrategy/RetryQuota.swift @@ -55,7 +55,7 @@ final actor RetryQuota { /// Creates a new quota with settings from the passed options. /// - Parameter options: The retry strategy options from which to configure this retry quota - convenience init(options: RetryStrategyOptions) { + init(options: RetryStrategyOptions) { self.init( availableCapacity: options.availableCapacity, maxCapacity: options.maxCapacity, diff --git a/Sources/ClientRuntime/Util/SwiftVersion.swift b/Sources/ClientRuntime/Util/SwiftVersion.swift index 6c167d527..6d1633b2c 100644 --- a/Sources/ClientRuntime/Util/SwiftVersion.swift +++ b/Sources/ClientRuntime/Util/SwiftVersion.swift @@ -49,20 +49,6 @@ private func swift5Version() -> String? { return "5.8" #elseif swift(>=5.7) return "5.7" - #elseif swift(>=5.6) - return "5.6" - #elseif swift(>=5.5) - return "5.5" - #elseif swift(>=5.4) - return "5.4" - #elseif swift(>=5.3) - return "5.3" - #elseif swift(>=5.2) - return "5.2" - #elseif swift(>=5.1) - return "5.1" - #elseif swift(>=5.0) - return "5.0" #else return nil #endif From b759e19c5c91f3e8156d3be117c3787f7b40db6d Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 11 Oct 2023 20:50:57 -0400 Subject: [PATCH 02/37] feat: support initial-response in RPC based event streams (#597) --- .../HttpResponseTraitWithoutHttpPayload.kt | 60 +++++++++++++++- .../EventStreamsInitialResponseTests.kt | 72 +++++++++++++++++++ .../MockHttpAWSJson11ProtocolGenerator.kt | 3 +- ...ent-stream-initial-request-response.smithy | 39 ++++++++++ 4 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 smithy-swift-codegen/src/test/kotlin/EventStreamsInitialResponseTests.kt create mode 100644 smithy-swift-codegen/src/test/resources/event-stream-initial-request-response.smithy 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 8cf4024a0..cd76dd545 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 @@ -50,15 +50,18 @@ class HttpResponseTraitWithoutHttpPayload( .filter { !it.member.hasTrait(HttpQueryTrait::class.java) } .toMutableSet() val streamingMember = bodyMembers.firstOrNull { it.member.targetOrSelf(ctx.model).hasTrait(StreamingTrait::class.java) } - if (streamingMember != null) { - writeStreamingMember(streamingMember) + val initialResponseMembers = bodyMembers.filter { + val targetShape = it.member.targetOrSelf(ctx.model) + targetShape?.hasTrait(StreamingTrait::class.java) == false + }.toSet() + writeStreamingMember(streamingMember, initialResponseMembers) } else if (bodyMembersWithoutQueryTrait.isNotEmpty()) { writeNonStreamingMembers(bodyMembersWithoutQueryTrait) } } - fun writeStreamingMember(streamingMember: HttpBindingDescriptor) { + fun writeStreamingMember(streamingMember: HttpBindingDescriptor, initialResponseMembers: Set) { val shape = ctx.model.expectShape(streamingMember.member.target) val symbol = ctx.symbolProvider.toSymbol(shape) val memberName = ctx.symbolProvider.toMemberName(streamingMember.member) @@ -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("}") @@ -133,4 +139,52 @@ class HttpResponseTraitWithoutHttpPayload( } private val path: String = "properties.".takeIf { outputShape.hasTrait() } ?: "" + + private fun writeInitialResponseMembers(initialResponseMembers: Set) { + 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", + ) } diff --git a/smithy-swift-codegen/src/test/kotlin/EventStreamsInitialResponseTests.kt b/smithy-swift-codegen/src/test/kotlin/EventStreamsInitialResponseTests.kt new file mode 100644 index 000000000..ad10072bd --- /dev/null +++ b/smithy-swift-codegen/src/test/kotlin/EventStreamsInitialResponseTests.kt @@ -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/TestStreamOperationWithInitialRequestResponseOutput+HttpResponseBinding.swift" + ) + contents.shouldSyntacticSanityCheck() + contents.shouldContainOnlyOnce( + """ + extension TestStreamOperationWithInitialRequestResponseOutput: 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(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 + } +} diff --git a/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpAWSJson11ProtocolGenerator.kt b/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpAWSJson11ProtocolGenerator.kt index 30f5c040a..04d70b2f4 100644 --- a/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpAWSJson11ProtocolGenerator.kt +++ b/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpAWSJson11ProtocolGenerator.kt @@ -53,7 +53,8 @@ class MockAWSJson11HttpProtocolCustomizations() : DefaultHttpProtocolCustomizati writer: SwiftWriter, op: OperationShape, ) { - TODO("Not yet implemented") + // Not yet implemented + return } } diff --git a/smithy-swift-codegen/src/test/resources/event-stream-initial-request-response.smithy b/smithy-swift-codegen/src/test/resources/event-stream-initial-request-response.smithy new file mode 100644 index 000000000..417462aa9 --- /dev/null +++ b/smithy-swift-codegen/src/test/resources/event-stream-initial-request-response.smithy @@ -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, +} \ No newline at end of file From e7f1d28de5d75104f1fdef5ed849e3799ea75fa6 Mon Sep 17 00:00:00 2001 From: AWS SDK Swift Automation Date: Thu, 12 Oct 2023 17:46:58 +0000 Subject: [PATCH 03/37] chore: Updates version to 0.32.0 --- Package.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.version b/Package.version index f0e1a438f..8a0d6d408 100644 --- a/Package.version +++ b/Package.version @@ -1 +1 @@ -0.31.0 \ No newline at end of file +0.32.0 \ No newline at end of file From f6c8bc807356e1d82aadc36e9c077079e5f0f3aa Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Thu, 12 Oct 2023 20:28:19 -0500 Subject: [PATCH 04/37] chore: Add newline to README.md (#602) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b919cec7a..b589f7cbc 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,4 @@ This project is licensed under the Apache-2.0 License. See [CONTRIBUTING](CONTRIBUTING.md) for more information. + From 62ad3a2745c48d4c8afa2ce96b99e0e124d72da1 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 20 Oct 2023 16:35:31 -0400 Subject: [PATCH 05/37] feat: add limited support in smithy-swift for visionOS (#606) --- Sources/ClientRuntime/Util/PlatformOperatingSystem.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/ClientRuntime/Util/PlatformOperatingSystem.swift b/Sources/ClientRuntime/Util/PlatformOperatingSystem.swift index 60a1782d3..0caf3b786 100644 --- a/Sources/ClientRuntime/Util/PlatformOperatingSystem.swift +++ b/Sources/ClientRuntime/Util/PlatformOperatingSystem.swift @@ -12,6 +12,7 @@ public enum PlatformOperatingSystem: String { case macOS case watchOS case tvOS + case visionOS case unknown } @@ -28,6 +29,8 @@ public var currentOS: PlatformOperatingSystem { return .windows #elseif os(tvOS) return .tvOS + #elseif os(visionOS) + return .visionOS #else #error("Cannot use a an operating system we do not support") #endif From 818a5c77d46328cad7460721e0cecf97d9a3af59 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 23 Oct 2023 15:14:32 -0400 Subject: [PATCH 06/37] feat: add support for requiresLength trait and Transfer-Encoding: Chunked (#604) --- .../Middlewares/ContentLengthMiddleware.swift | 19 +++- .../Networking/Http/SdkHttpRequest.swift | 3 + .../ContentLengthMiddlewareTests.swift | 85 ++++++++++++++ .../HttpBindingProtocolGenerator.kt | 51 ++++++--- .../middlewares/ContentLengthMiddleware.kt | 18 +-- .../HttpBindingProtocolGeneratorTests.kt | 3 +- .../HttpProtocolClientGeneratorTests.kt | 105 ++++++++++++++++++ .../service-generator-test-operations.smithy | 36 +++++- 8 files changed, 294 insertions(+), 26 deletions(-) create mode 100644 Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index d7e76b9ad..6fc759e95 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -6,7 +6,14 @@ public struct ContentLengthMiddleware private let contentLengthHeaderName = "Content-Length" - public init() {} + private var requiresLength: Bool = false + + private var unsignedPayload: Bool = false + + public init(requiresLength: Bool = false, unsignedPayload: Bool = false) { + self.requiresLength = requiresLength + self.unsignedPayload = unsignedPayload + } public func handle(context: Context, input: MInput, @@ -22,8 +29,16 @@ public struct ContentLengthMiddleware case .stream(let stream): if let length = stream.length { input.headers.update(name: "Content-Length", value: String(length)) + } else if !requiresLength && unsignedPayload { + // only for HTTP/1.1 requests, will be removed in all HTTP/2 requests + input.headers.update(name: "Transfer-Encoding", value: "Chunked") } else { - input.headers.update(name: "Transfer-Encoded", value: "Chunked") + let operation = context.attributes.get(key: AttributeKey(name: "Operation")) + ?? "Error getting operation name" + let errorMessage = unsignedPayload ? + "Missing content-length for operation: \(operation)" : + "Missing content-length for SigV4 signing on operation: \(operation)" + throw StreamError.notSupported(errorMessage) } default: input.headers.update(name: "Content-Length", value: "0") diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift index d6c5febcc..1c106cff9 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift @@ -75,6 +75,9 @@ extension SdkHttpRequest { httpRequest.path = [endpoint.path, endpoint.queryItemString].compactMap { $0 }.joined(separator: "?") httpRequest.addHeaders(headers: headers.toHttpHeaders()) + // Remove the "Transfer-Encoding" header if it exists since h2 does not support it + httpRequest.removeHeader(name: "Transfer-Encoding") + // HTTP2Request used with manual writes hence we need to set the body to nil // so that CRT does not write the body for us (we will write it manually) httpRequest.body = nil diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift new file mode 100644 index 000000000..3e5157523 --- /dev/null +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -0,0 +1,85 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +import XCTest +import SmithyTestUtil +@testable import ClientRuntime + +class ContentLengthMiddlewareTests: XCTestCase { + private var builtContext: HttpContext! + private var stack: OperationStack! + + override func setUpWithError() throws { + try super.setUpWithError() + builtContext = HttpContextBuilder() + .withMethod(value: .get) + .withPath(value: "/") + .withEncoder(value: JSONEncoder()) + .withDecoder(value: JSONDecoder()) + .withOperation(value: "Test Operation") + .build() + stack = OperationStack(id: "Test Operation") + } + + func testTransferEncodingChunkedSetWhenStreamLengthIsNil() async throws { + addContentLengthMiddlewareWith(requiresLength: false, unsignedPayload: true) + forceEmptyStream() + try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "Chunked"]) + } + + func testContentLengthSetWhenStreamLengthAvailableAndRequiresLengthSet() async throws { + addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: false) + try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) + } + + func testContentLengthSetWhenRequiresLengthAndUnsignedPayload() async throws { + addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: true) + try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) + } + + func testRequiresLengthSetWithNilStreamShouldThrowError() async throws { + addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: false) + forceEmptyStream() + do { + try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) + XCTFail("Should throw error") + } catch let error as StreamError { + switch error { + case .notSupported("Missing content-length for SigV4 signing on operation: Test Operation"), .notSupported("Missing content-length for operation: Test Operation"): + // The error matches one of the expected cases, test passes + break + default: + XCTFail("Error is not StreamError.notSupported with expected message") + } + } + } + + private func addContentLengthMiddlewareWith(requiresLength: Bool, unsignedPayload: Bool) { + stack.finalizeStep.intercept( + position: .before, + middleware: ContentLengthMiddleware(requiresLength: requiresLength, unsignedPayload: unsignedPayload) + ) + } + + private func forceEmptyStream() { + // Force stream length to be nil + stack.finalizeStep.intercept(position: .before, id: "set nil stream length") { (context, input, next) -> OperationOutput in + input.body = .stream(BufferedStream()) // Set the stream length to nil + return try await next.handle(context: context, input: input) + } + } + + private func AssertHeadersArePresent(expectedHeaders: [String: String], file: StaticString = #file, line: UInt = #line) async throws -> Void { + let mockHandler = MockHandler { (_, input) in + for (key, value) in expectedHeaders { + XCTAssert(input.headers.value(for: key) == value, file: file, line: line) + } + let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) + let output = OperationOutput(httpResponse: httpResponse, output: mockOutput) + return output + } + + _ = try await stack.handleMiddleware(context: builtContext, input: MockInput(), next: mockHandler) + } +} \ No newline at end of file diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index 0d1a7af38..cefe2bb5d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -4,6 +4,7 @@ */ package software.amazon.smithy.swift.codegen.integration +import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.knowledge.HttpBinding import software.amazon.smithy.model.knowledge.HttpBindingIndex @@ -30,6 +31,7 @@ import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait import software.amazon.smithy.model.traits.HttpQueryParamsTrait import software.amazon.smithy.model.traits.HttpQueryTrait import software.amazon.smithy.model.traits.MediaTypeTrait +import software.amazon.smithy.model.traits.RequiresLengthTrait import software.amazon.smithy.model.traits.StreamingTrait import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.swift.codegen.ClientRuntimeTypes @@ -60,6 +62,7 @@ import software.amazon.smithy.swift.codegen.integration.serde.UnionEncodeGenerat import software.amazon.smithy.swift.codegen.middleware.OperationMiddlewareGenerator import software.amazon.smithy.swift.codegen.model.ShapeMetadata import software.amazon.smithy.swift.codegen.model.bodySymbol +import software.amazon.smithy.swift.codegen.model.findStreamingMember import software.amazon.smithy.swift.codegen.model.hasEventStreamMember import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.utils.OptionalUtils @@ -91,9 +94,8 @@ fun formatHeaderOrQueryValue( memberShape: MemberShape, location: HttpBinding.Location, bindingIndex: HttpBindingIndex, - defaultTimestampFormat: TimestampFormatTrait.Format + defaultTimestampFormat: TimestampFormatTrait.Format, ): Pair { - return when (val shape = ctx.model.expectShape(memberShape.target)) { is TimestampShape -> { val timestampFormat = bindingIndex.determineTimestampFormat(memberShape, location, defaultTimestampFormat) @@ -165,7 +167,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { writer.openBlock( "extension $symbolName: \$N {", "}", - SwiftTypes.Protocols.Encodable + SwiftTypes.Protocols.Encodable, ) { writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) @@ -286,7 +288,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { private fun generateCodingKeysForMembers( ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, - members: List + members: List, ) { codingKeysGenerator.generateCodingKeysForMembers(ctx, writer, members) } @@ -298,7 +300,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { val inputType = ctx.model.expectShape(operation.input.get()) var metadata = mapOf( Pair(ShapeMetadata.OPERATION_SHAPE, operation), - Pair(ShapeMetadata.SERVICE_VERSION, ctx.service.version) + Pair(ShapeMetadata.SERVICE_VERSION, ctx.service.version), ) shapesInfo.put(inputType, metadata) } @@ -336,7 +338,6 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { } private fun resolveShapesNeedingCodableConformance(ctx: ProtocolGenerator.GenerationContext): Set { - val topLevelOutputMembers = getHttpBindingOperations(ctx).flatMap { val outputShape = ctx.model.expectShape(it.output.get()) outputShape.members() @@ -390,7 +391,8 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { RelationshipType.LIST_MEMBER, RelationshipType.SET_MEMBER, RelationshipType.MAP_VALUE, - RelationshipType.UNION_MEMBER -> true + RelationshipType.UNION_MEMBER, + -> true else -> false } }.forEach { @@ -403,6 +405,29 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { return resolved } + // Checks for @requiresLength trait + // Returns true if the operation: + // - has a streaming member with @httpPayload trait + // - target is a blob shape with @requiresLength trait + private fun hasRequiresLengthTrait(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean { + if (op.input.isPresent) { + val inputShape = ctx.model.expectShape(op.input.get()) + val streamingMember = inputShape.findStreamingMember(ctx.model) + if (streamingMember != null) { + val targetShape = ctx.model.expectShape(streamingMember.target) + if (targetShape != null) { + return streamingMember.hasTrait() && + targetShape.isBlobShape && + targetShape.hasTrait() + } + } + } + return false + } + + // Checks for @unsignedPayload trait on an operation + private fun hasUnsignedPayloadTrait(op: OperationShape): Boolean = op.hasTrait() + override fun generateProtocolClient(ctx: ProtocolGenerator.GenerationContext) { val symbol = ctx.symbolProvider.toSymbol(ctx.service) ctx.delegator.useFileWriter("./${ctx.settings.moduleName}/${symbol.name}.swift") { writer -> @@ -414,7 +439,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { serviceSymbol.name, defaultContentType, httpProtocolCustomizable, - operationMiddleware + operationMiddleware, ) clientGenerator.render() } @@ -433,7 +458,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { operationMiddleware.appendMiddleware(operation, ContentTypeMiddleware(ctx.model, ctx.symbolProvider, resolver.determineRequestContentType(operation))) operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx.model, ctx.symbolProvider)) - operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, shouldRenderEncodableConformance)) + operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, shouldRenderEncodableConformance, hasRequiresLengthTrait(ctx, operation), hasUnsignedPayloadTrait(operation))) operationMiddleware.appendMiddleware(operation, DeserializeMiddleware(ctx.model, ctx.symbolProvider)) operationMiddleware.appendMiddleware(operation, LoggingMiddleware(ctx.model, ctx.symbolProvider)) @@ -463,7 +488,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { members: List, writer: SwiftWriter, defaultTimestampFormat: TimestampFormatTrait.Format, - path: String? = null + path: String? = null, ) protected abstract fun renderStructDecode( ctx: ProtocolGenerator.GenerationContext, @@ -471,7 +496,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { members: List, writer: SwiftWriter, defaultTimestampFormat: TimestampFormatTrait.Format, - path: String + path: String, ) protected abstract fun addProtocolSpecificMiddleware(ctx: ProtocolGenerator.GenerationContext, operation: OperationShape) @@ -487,11 +512,11 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { for (operation in topDownIndex.getContainedOperations(ctx.service)) { OptionalUtils.ifPresentOrElse( Optional.of(getProtocolHttpBindingResolver(ctx, defaultContentType).httpTrait(operation)::class.java), - { containedOperations.add(operation) } + { containedOperations.add(operation) }, ) { LOGGER.warning( "Unable to fetch $protocolName protocol request bindings for ${operation.id} because " + - "it does not have an http binding trait" + "it does not have an http binding trait", ) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt index 22d70721f..af6975535 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt @@ -9,7 +9,7 @@ import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable import software.amazon.smithy.swift.codegen.middleware.MiddlewareStep -class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boolean) : MiddlewareRenderable { +class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boolean, private val requiresLength: Boolean, private val unsignedPayload: Boolean) : MiddlewareRenderable { override val name = "ContentLengthMiddleware" @@ -20,17 +20,17 @@ class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boo override fun render( writer: SwiftWriter, op: OperationShape, - operationStackName: String + operationStackName: String, ) { val hasHttpBody = MiddlewareShapeUtils.hasHttpBody(model, op) if (hasHttpBody || alwaysIntercept) { - writer.write( - "\$L.\$L.intercept(position: \$L, middleware: \$N())", - operationStackName, - middlewareStep.stringValue(), - position.stringValue(), - ClientRuntimeTypes.Middleware.ContentLengthMiddleware - ) + val str = "requiresLength: $requiresLength, unsignedPayload: $unsignedPayload" + val middlewareArgs = str.takeIf { requiresLength || unsignedPayload } ?: "" + + val interceptStatement = "$operationStackName.${middlewareStep.stringValue()}.intercept(" + + "position: ${position.stringValue()}, middleware: ${ClientRuntimeTypes.Middleware.ContentLengthMiddleware}($middlewareArgs))" + + writer.write(interceptStatement) } } } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt index 46e608384..4f996a72b 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt @@ -38,7 +38,7 @@ class TestHttpProtocolClientGeneratorFactory : HttpProtocolClientGeneratorFactor private fun getClientProperties(ctx: ProtocolGenerator.GenerationContext): List { return mutableListOf( DefaultRequestEncoder(), - DefaultResponseDecoder() + DefaultResponseDecoder(), ) } @@ -125,6 +125,7 @@ extension InlineDocumentAsPayloadOutput: ClientRuntime.HttpResponseBinding { """.trimIndent() contents.shouldContainOnlyOnce(expectedContents) } + @Test fun `default fooMap to an empty map if keysForFooMap is empty`() { val contents = getModelFileContents("example", "HttpPrefixHeadersOutput+HttpResponseBinding.swift", newTestContext.manifest) diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index 1e46c20a7..208f816c6 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -148,6 +148,111 @@ class HttpProtocolClientGeneratorTests { contents.shouldContainOnlyOnce(expected) } + @Test + fun `ContentLengthMiddleware generates correctly with requiresLength false and unsignedPayload true`() { + val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") + contents.shouldSyntacticSanityCheck() + val expected = + """ + public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStream") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "GetFooStreamingRequest")) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: false, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + """.trimIndent() + contents.shouldContainOnlyOnce(expected) + } + + @Test + fun `ContentLengthMiddleware generates correctly with requiresLength true and unsignedPayload false`() { + val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") + contents.shouldSyntacticSanityCheck() + val expected = + """ + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) + operation.serializeStep.intercept(position: .after, middleware: UnsignedFooBlobStreamWithLengthInputBodyMiddleware()) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + """.trimIndent() + contents.shouldContainOnlyOnce(expected) + } + + @Test + fun `ContentLengthMiddleware generates correctly with requiresLength true and unsignedPayload true`() { + val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") + contents.shouldSyntacticSanityCheck() + val expected = + """ + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) + operation.serializeStep.intercept(position: .after, middleware: UnsignedFooBlobStreamWithLengthInputBodyMiddleware()) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + """.trimIndent() + contents.shouldContainOnlyOnce(expected) + } + private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { val context = TestContext.initContextFrom(smithyFile, serviceShapeId, MockHttpRestJsonProtocolGenerator()) { model -> model.defaultSettings(serviceShapeId, "RestJson", "2019-12-16", "Rest Json Protocol") diff --git a/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy b/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy index 537e5531c..f1a6f4e71 100644 --- a/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy +++ b/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy @@ -2,6 +2,7 @@ $version: "1.0" namespace com.test use aws.protocols#awsJson1_1 +use aws.auth#unsignedPayload @awsJson1_1 service Example { @@ -15,7 +16,10 @@ service Example { GetFooStreamingOutputNoInput, GetFooStreamingInputNoOutput, AllocateWidget, - OperationWithDeprecatedTrait + OperationWithDeprecatedTrait, + UnsignedFooBlobStream, + UnsignedFooBlobStreamWithLength, + ExplicitBlobStreamWithLength ] } @@ -89,4 +93,34 @@ operation AllocateWidget { structure AllocateWidgetInput { @idempotencyToken clientToken: String +} + +// Stream must have a known size +@streaming +@requiresLength +blob BodyStreamWithLength + +@http(method: "POST", uri: "/explicit/blobstreamunsigned") +@unsignedPayload +operation UnsignedFooBlobStream { + input: GetFooStreamingRequest, + output: GetFooStreamingResponse +} + +@http(method: "POST", uri: "/explicit/blobstreamunsignedwithlength") +@unsignedPayload +operation UnsignedFooBlobStreamWithLength { + input: ExplicitBlobStreamWithLengthRequest, + output: GetFooStreamingResponse +} + +@http(method: "POST", uri: "/explicit/blobstreamwithlength") +operation ExplicitBlobStreamWithLength { + input: ExplicitBlobStreamWithLengthRequest, + output: GetFooStreamingResponse +} + +structure ExplicitBlobStreamWithLengthRequest { + @httpPayload + payload1: BodyStreamWithLength } \ No newline at end of file From e0b4f254ad352db267995d9a40b1ff83a6006e4e Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 24 Oct 2023 16:51:57 -0500 Subject: [PATCH 07/37] chore: Update to aws-crt-swift 0.15.0 (#607) --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index e4ed89dd5..9df59d6b9 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]) ], dependencies: [ - .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.13.0"), + .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.15.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", exact: "0.17.0") ], From 31d7652e622dbcb561221281b0309f059e1a6201 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 25 Oct 2023 09:06:48 -0400 Subject: [PATCH 08/37] fix: content-length middleware should not error on event streams (#608) --- .../Middlewares/ContentLengthMiddleware.swift | 21 +++++++++++++------ .../ContentLengthMiddlewareTests.swift | 9 +++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index 6fc759e95..ef39978a2 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -6,11 +6,17 @@ public struct ContentLengthMiddleware private let contentLengthHeaderName = "Content-Length" - private var requiresLength: Bool = false + private var requiresLength: Bool? - private var unsignedPayload: Bool = false + private var unsignedPayload: Bool? - public init(requiresLength: Bool = false, unsignedPayload: Bool = false) { + /// Creates a new `ContentLengthMiddleware` with the supplied parameters + /// - Parameters: + /// - requiresLength: Trait requires the length of a blob stream to be known. + /// When the request body is not a streaming blob, `nil` should be passed. Defaults to `nil`. + /// - unsignedPayload: Trait signifies that the length of a stream in payload does not need to be known. + /// When the request body is not a streaming blob, `nil` should be passed. Defaults to `nil`. + public init(requiresLength: Bool? = nil, unsignedPayload: Bool? = nil) { self.requiresLength = requiresLength self.unsignedPayload = unsignedPayload } @@ -29,13 +35,16 @@ public struct ContentLengthMiddleware case .stream(let stream): if let length = stream.length { input.headers.update(name: "Content-Length", value: String(length)) - } else if !requiresLength && unsignedPayload { - // only for HTTP/1.1 requests, will be removed in all HTTP/2 requests + } else if (requiresLength == false && unsignedPayload == true) || + (requiresLength == nil && unsignedPayload == nil) { + // Transfer-Encoding can be sent on all Event Streams where length cannot be determined + // or on blob Data Streams where requiresLength is true and unsignedPayload is false + // Only for HTTP/1.1 requests, will be removed in all HTTP/2 requests input.headers.update(name: "Transfer-Encoding", value: "Chunked") } else { let operation = context.attributes.get(key: AttributeKey(name: "Operation")) ?? "Error getting operation name" - let errorMessage = unsignedPayload ? + let errorMessage = (unsignedPayload ?? false) ? "Missing content-length for operation: \(operation)" : "Missing content-length for SigV4 signing on operation: \(operation)" throw StreamError.notSupported(errorMessage) diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift index 3e5157523..e2b48fe23 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -27,6 +27,13 @@ class ContentLengthMiddlewareTests: XCTestCase { try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "Chunked"]) } + func testTransferEncodingChunkedSetWithNilTraits() async throws { + // default constructor + addContentLengthMiddlewareWith(requiresLength: nil, unsignedPayload: nil) + forceEmptyStream() + try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "Chunked"]) + } + func testContentLengthSetWhenStreamLengthAvailableAndRequiresLengthSet() async throws { addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: false) try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) @@ -54,7 +61,7 @@ class ContentLengthMiddlewareTests: XCTestCase { } } - private func addContentLengthMiddlewareWith(requiresLength: Bool, unsignedPayload: Bool) { + private func addContentLengthMiddlewareWith(requiresLength: Bool?, unsignedPayload: Bool?) { stack.finalizeStep.intercept( position: .before, middleware: ContentLengthMiddleware(requiresLength: requiresLength, unsignedPayload: unsignedPayload) From 4d965ac32af105f48061b85eb36427b0c66be192 Mon Sep 17 00:00:00 2001 From: AWS SDK Swift Automation Date: Wed, 25 Oct 2023 19:16:18 +0000 Subject: [PATCH 09/37] chore: Updates version to 0.33.0 --- Package.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.version b/Package.version index 8a0d6d408..7d07a1960 100644 --- a/Package.version +++ b/Package.version @@ -1 +1 @@ -0.32.0 \ No newline at end of file +0.33.0 \ No newline at end of file From 53fbde27f536f4287edfd4ff08bb5ebe8dc75169 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 25 Oct 2023 18:51:13 -0500 Subject: [PATCH 10/37] chore: Improved downstream task (#568) --- .github/workflows/continuous-integration.yml | 145 +++++++++++++++---- scripts/ci_steps/select_dependency_branch.sh | 33 +++++ 2 files changed, 152 insertions(+), 26 deletions(-) create mode 100755 scripts/ci_steps/select_dependency_branch.sh diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 64cecb321..e5c851b65 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -7,43 +7,109 @@ on: workflow_dispatch: env: - BUILDER_VERSION: v0.9.11 - BUILDER_SOURCE: releases - # host owned by CRT team to host aws-crt-builder releases. Contact their on-call with any issues - BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net - PACKAGE_NAME: smithy-swift - LINUX_BASE_IMAGE: ubuntu-16-x64 - RUN: ${{ github.run_id }}-${{ github.run_number }} - AWS_SDK_SWIFT_CI_DIR: /Users/runner/work/smithy-swift/smithy-swift/target/build/deps/aws-sdk-swift - AWS_CRT_SWIFT_CI_DIR: /Users/runner/work/smithy-swift/smithy-swift/target/build/deps/aws-crt-swift - SMITHY_SWIFT_CI_DIR: /Users/runner/work/smithy-swift/smithy-swift + AWS_SWIFT_SDK_USE_LOCAL_DEPS: 1 jobs: downstream: - runs-on: macos-13 + runs-on: ${{ matrix.runner }} env: - DEVELOPER_DIR: /Applications/Xcode_14.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer + strategy: + fail-fast: false + matrix: + # This matrix runs tests on iOS sim & Mac, on oldest & newest supported Xcodes + runner: + - macos-12 + - macos-13 + xcode: + - Xcode_14.0.1 + - Xcode_15.0 + destination: + - 'platform=iOS Simulator,OS=16.0,name=iPhone 13' + - 'platform=iOS Simulator,OS=17.0,name=iPhone 15' + - 'platform=OS X' + exclude: + # Don't run old macOS with new Xcode + - runner: macos-12 + xcode: Xcode_15.0 + # Don't run new macOS with old Xcode + - runner: macos-13 + xcode: Xcode_14.0.1 + # Don't run old iOS simulator with new Xcode + - destination: 'platform=iOS Simulator,OS=16.0,name=iPhone 13' + xcode: Xcode_15.0 + # Don't run new iOS simulator with old Xcode + - destination: 'platform=iOS Simulator,OS=17.0,name=iPhone 15' + xcode: Xcode_14.0.1 steps: - - name: Checkout sources + - name: Checkout smithy-swift uses: actions/checkout@v3 - - uses: actions/cache@v3 + - name: Select aws-sdk-swift branch + run: | + ORIGINAL_REPO_HEAD_REF="$GITHUB_HEAD_REF" \ + DEPENDENCY_REPO_URL="https://github.com/awslabs/aws-sdk-swift.git" \ + ./scripts/ci_steps/select_dependency_branch.sh + - name: Checkout aws-sdk-swift + uses: actions/checkout@v3 + with: + repository: awslabs/aws-sdk-swift + ref: ${{ env.DEPENDENCY_REPO_SHA }} + path: aws-sdk-swift + - name: Move aws-sdk-swift into place + run: mv aws-sdk-swift .. + - name: Cache Gradle + uses: actions/cache@v3 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + key: 1-${{ runner.os }}-gradle-${{ hashFiles('settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + 1-${{ runner.os }}-gradle-${{ hashFiles('settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }} + 1-${{ runner.os }}-gradle- + - name: Cache Swift + uses: actions/cache@v3 + with: + path: | + ~/Library/Caches/org.swift.swiftpm + ~/.cache/org.swift.swiftpm + key: 1-${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('Package.swift') }} restore-keys: | - ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} - ${{ runner.os }}-gradle- - - uses: actions/setup-java@v3 + 1-${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('Package.swift') }} + 1-${{ runner.os }}-${{ matrix.xcode }}-spm- + - name: Setup Java + uses: actions/setup-java@v3 with: distribution: corretto java-version: 17 - - name: Build and Test ${{ env.PACKAGE_NAME }} Downstream Consumers + - name: Tools Versions + run: | + cd ../aws-sdk-swift + ./scripts/ci_steps/log_tool_versions.sh + - name: Build & Run smithy-swift Kotlin Unit Tests + run: ./gradlew build + - name: Build & Run smithy-swift Swift Unit Tests + run: | + set -o pipefail && \ + NSUnbufferedIO=YES xcodebuild \ + -scheme smithy-swift-Package \ + -destination '${{ matrix.destination }}' \ + test 2>&1 \ + | xcpretty + - name: Prepare aws-sdk-swift Protocol & Unit Tests run: | - python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')" - chmod a+x builder - AWS_CRT_SWIFT_CI_DIR="${{ env.AWS_CRT_SWIFT_CI_DIR }}" AWS_SDK_SWIFT_CI_DIR="${{ env.AWS_SDK_SWIFT_CI_DIR }}" SMITHY_SWIFT_CI_DIR="${{ env.SMITHY_SWIFT_CI_DIR }}" AWS_SWIFT_SDK_USE_LOCAL_DEPS=1 ./builder build -p ${{ env.PACKAGE_NAME }} --spec downstream + cd ../aws-sdk-swift + ./scripts/ci_steps/prepare_protocol_and_unit_tests.sh + - name: Build and Run aws-sdk-swift Protocol & Unit Tests + run: | + cd ../aws-sdk-swift + set -o pipefail && \ + NSUnbufferedIO=YES xcodebuild \ + -scheme aws-sdk-swift \ + -destination '${{ matrix.destination }}' \ + test 2>&1 \ + | xcpretty + linux: runs-on: ubuntu-latest strategy: @@ -57,14 +123,41 @@ jobs: container: image: swift:${{ matrix.swift }} steps: + - name: Checkout Sources + uses: actions/checkout@v3 - name: Install openssl run: | if [ -x "$(command -v apt)" ]; then apt-get update && apt-get install -y libssl-dev else - yum install -y openssl-devel + yum install -y openssl-devel which fi - - name: Checkout Sources - uses: actions/checkout@v3 - - name: Build and Test + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: 1-${{ runner.os }}-gradle-${{ hashFiles('settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + 1-${{ runner.os }}-gradle-${{ hashFiles('settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }} + 1-${{ runner.os }}-gradle- + - name: Cache Swift + uses: actions/cache@v3 + with: + path: | + ~/Library/Caches/org.swift.swiftpm + ~/.cache/org.swift.swiftpm + key: 1-${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('Package.swift') }} + restore-keys: | + 1-${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('Package.swift') }} + 1-${{ runner.os }}-${{ matrix.xcode }}-spm- + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: corretto + java-version: 17 + - name: Build & Run Kotlin Unit Tests + run: ./gradlew build + - name: Build & Run Swift Unit Tests run: swift test diff --git a/scripts/ci_steps/select_dependency_branch.sh b/scripts/ci_steps/select_dependency_branch.sh new file mode 100755 index 000000000..eb1d534bb --- /dev/null +++ b/scripts/ci_steps/select_dependency_branch.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + +# This script selects the Git reference for a locally installed dependency of another "original" repo. +# It matches the original's checked-out branch name if available, and if not, falls back to main. + +# Checks to see if Git repository at DEPENDENCY_REPO_URL has a branch named ORIGINAL_REPO_HEAD_REF. +# If so, the latest SHA for that branch is obtained and stored as DEPENDENCY_REPO_SHA in the Github env. +# +# If a branch named ORIGINAL_REPO_HEAD_REF does not exist in the Git repository at DEPENDENCY_REPO_URL, +# the SHA for the main branch is obtained and stored as DEPENDENCY_REPO_SHA in the Github env. + +# Parameters: +# ORIGINAL_REPO_HEAD_REF : the branch or tag name for the original repo branch being built. +# DEPENDENCY_REPO_URL : the URL to the dependency that will be matched to the branch + +# Output: +# DEPENDENCY_REPO_SHA : the Git SHA for the dependency repo commit to be built (set in the Github environment) + +echo "Finding correct branch for dependency repo: $DEPENDENCY_REPO_URL" +DEPENDENCY_BRANCH_SHA=`git ls-remote --heads "$DEPENDENCY_REPO_URL" "refs/heads/$ORIGINAL_REPO_HEAD_REF" | awk '{print $1}'` +if [[ ! -z "${DEPENDENCY_BRANCH_SHA}" ]]; then + echo "Ref $ORIGINAL_REPO_HEAD_REF was found on dependency repo at SHA $DEPENDENCY_BRANCH_SHA" + echo "Selecting dependency repo branch $ORIGINAL_REPO_HEAD_REF at $DEPENDENCY_BRANCH_SHA" + echo "DEPENDENCY_REPO_SHA=$DEPENDENCY_BRANCH_SHA" >> "$GITHUB_ENV" +else + echo "Ref $ORIGINAL_REPO_HEAD_REF was not found on dependency repo at SHA $DEPENDENCY_BRANCH_SHA" + DEPENDENCY_MAIN_SHA=`git ls-remote --heads "$ORIGINAL_REPO_HEAD_REF" "refs/heads/main" | awk '{print $1}'` + echo "Selecting dependency repo main branch at $DEPENDENCY_MAIN_SHA" + echo "DEPENDENCY_REPO_SHA=$DEPENDENCY_MAIN_SHA" >> "$GITHUB_ENV" +fi + From ef962ddd6ab98e6bbde5f1708ad4b2ad74c9cf1c Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Mon, 30 Oct 2023 16:42:53 -0500 Subject: [PATCH 11/37] chore: Convert idempotency token middleware from closure to reusable type (#610) --- .../IdempotencyTokenMiddleware.swift | 33 +++++++ .../IdempotencyTokenMiddlewareTests.swift | 86 +++++++++++++++++++ .../swift/codegen/ClientRuntimeTypes.kt | 1 + .../middlewares/IdempotencyTokenMiddleware.kt | 25 +++--- .../test/kotlin/ContentMd5MiddlewareTests.kt | 9 +- .../HttpProtocolClientGeneratorTests.kt | 9 +- .../test/kotlin/IdempotencyTokenTraitTests.kt | 9 +- 7 files changed, 136 insertions(+), 36 deletions(-) create mode 100644 Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift create mode 100644 Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift diff --git a/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift b/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift new file mode 100644 index 000000000..a8a536121 --- /dev/null +++ b/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift @@ -0,0 +1,33 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct IdempotencyTokenMiddleware: ClientRuntime.Middleware { + public let id: Swift.String = "IdempotencyTokenMiddleware" + private let keyPath: WritableKeyPath + + public init(keyPath: WritableKeyPath) { + self.keyPath = keyPath + } + + public func handle(context: Context, + input: MInput, + next: H) async throws -> MOutput + where H: Handler, Self.MInput == H.Input, Self.MOutput == H.Output, Self.Context == H.Context { + var copiedInput = input + if input[keyPath: keyPath] == nil { + let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator() + copiedInput[keyPath: keyPath] = idempotencyTokenGenerator.generateToken() + } + return try await next.handle(context: context, input: copiedInput) + } + + public typealias MInput = OperationStackInput + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} diff --git a/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift b/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift new file mode 100644 index 000000000..1728c396e --- /dev/null +++ b/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift @@ -0,0 +1,86 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import ClientRuntime +import XCTest + +class IdempotencyTokenMiddlewareTests: XCTestCase { + + private typealias Subject = IdempotencyTokenMiddleware + + let token = "def" + let previousToken = "abc" + private var tokenGenerator: IdempotencyTokenGenerator! + private var context: HttpContext! + private var subject: Subject! + + override func setUp() async throws { + try await super.setUp() + tokenGenerator = TestIdempotencyTokenGenerator(token: token) + context = HttpContextBuilder().withIdempotencyTokenGenerator(value: tokenGenerator).build() + subject = Subject(keyPath: \.tokenMember) + } + + func test_handle_itSetsAnIdempotencyTokenIfNoneIsSet() async throws { + let input = TestInputType(tokenMember: nil) + let next = MockHandler { (context, input) in + XCTAssertEqual(input.tokenMember, self.token) + } + _ = try await subject.handle(context: context, input: input, next: next) + } + + func test_handle_itDoesNotChangeTheIdempotencyTokenIfAlreadySet() async throws { + let input = TestInputType(tokenMember: previousToken) + let next = MockHandler { (context, input) in + XCTAssertEqual(input.tokenMember, self.previousToken) + } + _ = try await subject.handle(context: context, input: input, next: next) + } +} + +// MARK: - Test fixtures & types + +private struct TestIdempotencyTokenGenerator: IdempotencyTokenGenerator { + let token: String + func generateToken() -> String { token } +} + +private struct TestInputType { + var tokenMember: String? +} + +private struct TestOutputType: HttpResponseBinding { + init(httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder?) async throws { + // no-op + } +} + +private enum TestOutputErrorType: HttpResponseErrorBinding { + static func makeError(httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder?) async throws -> Error { + return TestError() + } +} + +private struct TestError: Error {} + +private struct MockHandler: Handler { + typealias Output = OperationOutput + typealias Context = HttpContext + typealias MockHandlerCallback = (Context, I) async throws -> Void + + private let handleCallback: MockHandlerCallback + + init(handleCallback: @escaping MockHandlerCallback) { + self.handleCallback = handleCallback + } + + func handle(context: Context, input: I) async throws -> Output { + try await handleCallback(context, input) + return OperationOutput(httpResponse: HttpResponse()) + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt index d4ae215c7..be7075e70 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt @@ -75,6 +75,7 @@ object ClientRuntimeTypes { val HeaderMiddleware = runtimeSymbol("HeaderMiddleware") val SerializableBodyMiddleware = runtimeSymbol("SerializableBodyMiddleware") val RetryMiddleware = runtimeSymbol("RetryMiddleware") + val IdempotencyTokenMiddleware = runtimeSymbol("IdempotencyTokenMiddleware") val NoopHandler = runtimeSymbol("NoopHandler") object Providers { diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt index 9f9963187..3fb6e4025 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt @@ -24,25 +24,26 @@ class IdempotencyTokenMiddleware( override val name = "IdempotencyTokenMiddleware" override val middlewareStep = MiddlewareStep.INITIALIZESTEP override val position = MiddlewarePosition.AFTER - override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) { + override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) { val inputShape = model.expectShape(op.input.get()) val idempotentMember = inputShape.members().firstOrNull { it.hasTrait() } idempotentMember?.let { val idempotentMemberName = it.memberName.decapitalize() + val inputShapeName = MiddlewareShapeUtils.inputSymbol(symbolProvider, model, op).name val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name val outputErrorShapeName = MiddlewareShapeUtils.outputErrorSymbolName(op) - writer.openBlock( - "$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, id: \"${name}\") { (context, input, next) -> \$N<$outputShapeName> in", "}", - ClientRuntimeTypes.Middleware.OperationOutput - ) { - writer.write("let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator()") - writer.write("var copiedInput = input") - writer.openBlock("if input.$idempotentMemberName == nil {", "}") { - writer.write("copiedInput.$idempotentMemberName = idempotencyTokenGenerator.generateToken()") - } - writer.write("return try await next.handle(context: context, input: copiedInput)") - } + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L, \$L>(keyPath: \\.\$L))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.IdempotencyTokenMiddleware, + inputShapeName, + outputShapeName, + outputErrorShapeName, + idempotentMemberName + ) } } } diff --git a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt index cee419a12..13eeeb1d2 100644 --- a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt @@ -27,14 +27,7 @@ class ContentMd5MiddlewareTests { .withPartitionID(value: config.partitionID) .build() var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") - operation.initializeStep.intercept(position: .after, id: "IdempotencyTokenMiddleware") { (context, input, next) -> ClientRuntime.OperationOutput in - let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator() - var copiedInput = input - if input.token == nil { - copiedInput.token = idempotencyTokenGenerator.generateToken() - } - return try await next.handle(context: context, input: copiedInput) - } + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.ContentMD5Middleware()) diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index 208f816c6..cb578f9b3 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -125,14 +125,7 @@ class HttpProtocolClientGeneratorTests { .withPartitionID(value: config.partitionID) .build() var operation = ClientRuntime.OperationStack(id: "allocateWidget") - operation.initializeStep.intercept(position: .after, id: "IdempotencyTokenMiddleware") { (context, input, next) -> ClientRuntime.OperationOutput in - let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator() - var copiedInput = input - if input.clientToken == nil { - copiedInput.clientToken = idempotencyTokenGenerator.generateToken() - } - return try await next.handle(context: context, input: copiedInput) - } + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.clientToken)) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) diff --git a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt index 4ec4980a1..bf6db1240 100644 --- a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt @@ -27,14 +27,7 @@ class IdempotencyTokenTraitTests { .withPartitionID(value: config.partitionID) .build() var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") - operation.initializeStep.intercept(position: .after, id: "IdempotencyTokenMiddleware") { (context, input, next) -> ClientRuntime.OperationOutput in - let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator() - var copiedInput = input - if input.token == nil { - copiedInput.token = idempotencyTokenGenerator.generateToken() - } - return try await next.handle(context: context, input: copiedInput) - } + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml")) From a35c9835de1147a8708309cf926332e788981aed Mon Sep 17 00:00:00 2001 From: Chan Yoo <55515281+sichanyoo@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:47:08 -0700 Subject: [PATCH 12/37] fix: Update aws-crt-swift dependency to 0.17.0 (#612) --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 9df59d6b9..36e9d0bc3 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,7 @@ let package = Package( .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]) ], dependencies: [ - .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.15.0"), + .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.17.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", exact: "0.17.0") ], From 6037a8157082d00da6ff1be1462022dcaccb3662 Mon Sep 17 00:00:00 2001 From: AWS SDK Swift Automation Date: Tue, 31 Oct 2023 02:14:02 +0000 Subject: [PATCH 13/37] chore: Updates version to 0.34.0 --- Package.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.version b/Package.version index 7d07a1960..75544d772 100644 --- a/Package.version +++ b/Package.version @@ -1 +1 @@ -0.33.0 \ No newline at end of file +0.34.0 \ No newline at end of file From 96b95c1011341d141718f1b3f34a7d15318ed550 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 31 Oct 2023 15:41:27 -0400 Subject: [PATCH 14/37] fix: Endpoint url should be nil if host or scheme is missing (#614) --- Sources/ClientRuntime/Networking/Endpoint.swift | 4 ++-- .../NetworkingTests/HttpRequestTests.swift | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Endpoint.swift b/Sources/ClientRuntime/Networking/Endpoint.swift index f51cf8dd5..9dbb65857 100644 --- a/Sources/ClientRuntime/Networking/Endpoint.swift +++ b/Sources/ClientRuntime/Networking/Endpoint.swift @@ -63,10 +63,10 @@ extension Endpoint { public var url: URL? { var components = URLComponents() components.scheme = protocolType?.rawValue - components.host = host + components.host = host.isEmpty ? nil : host // If host is empty, URL is invalid components.percentEncodedPath = path components.percentEncodedQuery = queryItemString - return components.url + return (components.host == nil || components.scheme == nil) ? nil : components.url } var queryItemString: String? { diff --git a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift index 2c07eb4b0..d2f021029 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift @@ -111,7 +111,9 @@ class HttpRequestTests: NetworkingTestUtils { } func testConversionToUrlRequestFailsWithInvalidEndpoint() { - // TODO:: When is the endpoint invalid or endpoint.url nil? - _ = Endpoint(host: "", path: "", protocolType: nil) + // Testing with an invalid endpoint where host is empty, + // path is empty, and protocolType is nil. + let endpoint = Endpoint(host: "", path: "", protocolType: nil) + XCTAssertNil(endpoint.url, "An invalid endpoint should result in a nil URL.") } } From a03eedc088a19bbab5fb72c0af83d4c6c1ff49fe Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Thu, 2 Nov 2023 11:12:05 -0500 Subject: [PATCH 15/37] fix: Pool HTTP connections based on scheme, host, and port (#615) --- .../Networking/Http/CRT/CRTClientEngine.swift | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 836d35d40..b89c954c6 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -12,12 +12,31 @@ import Darwin public class CRTClientEngine: HttpClientEngine { actor SerialExecutor { + + /// Stores the common properties of requests that should share a HTTP connection, such that requests + /// with equal `ConnectionID` values should be pooled together. + /// + /// Used as a dictionary key for storing CRT connection managers once they have been created. + /// When a new request is made, a connection manager is reused if it matches the request's scheme, + /// host, and port. + private struct ConnectionPoolID: Hashable { + private let protocolType: ProtocolType? + private let host: String + private let port: Int16 + + init(endpoint: Endpoint) { + self.protocolType = endpoint.protocolType + self.host = endpoint.host + self.port = endpoint.port + } + } + private var logger: LogAgent private let windowSize: Int private let maxConnectionsPerEndpoint: Int - private var connectionPools: [Endpoint: HTTPClientConnectionManager] = [:] - private var http2ConnectionPools: [Endpoint: HTTP2StreamManager] = [:] + private var connectionPools: [ConnectionPoolID: HTTPClientConnectionManager] = [:] + private var http2ConnectionPools: [ConnectionPoolID: HTTP2StreamManager] = [:] private let sharedDefaultIO = SDKDefaultIO.shared private let connectTimeoutMs: UInt32? @@ -29,22 +48,22 @@ public class CRTClientEngine: HttpClientEngine { } func getOrCreateConnectionPool(endpoint: Endpoint) throws -> HTTPClientConnectionManager { - guard let connectionPool = connectionPools[endpoint] else { + let poolID = ConnectionPoolID(endpoint: endpoint) + guard let connectionPool = connectionPools[poolID] else { let newConnectionPool = try createConnectionPool(endpoint: endpoint) - connectionPools[endpoint] = newConnectionPool // save in dictionary + connectionPools[poolID] = newConnectionPool // save in dictionary return newConnectionPool } - return connectionPool } func getOrCreateHTTP2ConnectionPool(endpoint: Endpoint) throws -> HTTP2StreamManager { - guard let connectionPool = http2ConnectionPools[endpoint] else { + let poolID = ConnectionPoolID(endpoint: endpoint) + guard let connectionPool = http2ConnectionPools[poolID] else { let newConnectionPool = try createHTTP2ConnectionPool(endpoint: endpoint) - http2ConnectionPools[endpoint] = newConnectionPool // save in dictionary + http2ConnectionPools[poolID] = newConnectionPool // save in dictionary return newConnectionPool } - return connectionPool } From c19f585d19dddc4f52a742077bd559cc1da425cc Mon Sep 17 00:00:00 2001 From: Cyprien Ricque <48893621+CyprienRicque@users.noreply.github.com> Date: Tue, 7 Nov 2023 04:30:24 +0800 Subject: [PATCH 16/37] add default log level to initialize method (#616) --- Sources/ClientRuntime/Logging/SDKLoggingSystem.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift b/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift index 584011547..a408f5a4e 100644 --- a/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift +++ b/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift @@ -15,13 +15,13 @@ public class SDKLoggingSystem { factories[label] = logHandlerFactory } - public class func initialize() { + public class func initialize(defaultLogLevel: SDKLogLevel = .info) { LoggingSystem.bootstrap { label in if let factory = factories[label] { return factory.construct(label: label) } var handler = StreamLogHandler.standardOutput(label: label) - handler.logLevel = .info + handler.logLevel = defaultLogLevel.toLoggerType() return handler } } From c2ab0a55310aca3ab79aacb762df2a2e5f41e397 Mon Sep 17 00:00:00 2001 From: Chan Yoo <55515281+sichanyoo@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:38:14 -0800 Subject: [PATCH 17/37] feat: add utility method for converting SdkHttpRequest to URLRequest. (#613) * Add extension constructor to URLRequest to convert SDKHttpRequest * Add preprocessor conditional import functionality to SwiftWriter. --------- Co-authored-by: Sichan Yoo --- .../Networking/Http/SdkHttpRequest.swift | 30 +++++++++++++++++++ .../NetworkingTests/HttpRequestTests.swift | 30 +++++++++++++++++++ .../swift/codegen/ImportDeclarations.kt | 26 ++++++++++++++-- .../smithy/swift/codegen/SwiftWriter.kt | 9 ++++-- 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift index 1c106cff9..6a45a3067 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift @@ -6,6 +6,12 @@ import struct Foundation.CharacterSet import struct Foundation.URLQueryItem import struct Foundation.URLComponents import AwsCommonRuntimeKit +// In Linux, Foundation.URLRequest is moved to FoundationNetworking. +#if canImport(FoundationNetworking) +import FoundationNetworking +#else +import struct Foundation.URLRequest +#endif // we need to maintain a reference to this same request while we add headers // in the CRT engine so that is why it's a class @@ -85,6 +91,30 @@ extension SdkHttpRequest { } } +public extension URLRequest { + init(sdkRequest: SdkHttpRequest) async throws { + // Set URL + guard let url = sdkRequest.endpoint.url else { + throw ClientError.dataNotFound("Failed to construct URLRequest due to missing URL.") + } + self.init(url: url) + // Set method type + self.httpMethod = sdkRequest.method.rawValue + // Set body, handling any serialization errors + do { + self.httpBody = try await sdkRequest.body.readData() + } catch { + throw ClientError.serializationFailed("Failed to construct URLRequest due to HTTP body conversion failure.") + } + // Set headers + sdkRequest.headers.headers.forEach { header in + header.value.forEach { value in + self.addValue(value, forHTTPHeaderField: header.name) + } + } + } +} + extension SdkHttpRequest: CustomDebugStringConvertible, CustomStringConvertible { public var debugDescriptionWithBody: String { diff --git a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift index d2f021029..c1bc09257 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift @@ -7,6 +7,12 @@ import XCTest import AwsCommonRuntimeKit import struct Foundation.URLQueryItem @testable import ClientRuntime +// In Linux, Foundation.URLRequest is moved to FoundationNetworking. +#if canImport(FoundationNetworking) +import FoundationNetworking +#else +import struct Foundation.URLRequest +#endif class HttpRequestTests: NetworkingTestUtils { @@ -51,6 +57,30 @@ class HttpRequestTests: NetworkingTestUtils { } } + func testSdkHttpRequestToURLRequest() async throws { + let headers = Headers(["Testname-1": "testvalue-1", "Testname-2": "testvalue-2"]) + let endpoint = Endpoint(host: "host.com", path: "/", headers: headers) + + let httpBody = HttpBody.data(expectedMockRequestData) + let mockHttpRequest = SdkHttpRequest(method: .get, endpoint: endpoint, body: httpBody) + let urlRequest = try await URLRequest(sdkRequest: mockHttpRequest) + + XCTAssertNotNil(urlRequest) + guard let headersFromRequest = urlRequest.allHTTPHeaderFields else { + XCTFail("Headers in SdkHttpRequest were not successfully converted to headers in URLRequest.") + // Compiler doesn't recognize XCTFail as return / exception thrown + return + } + + // Check URLRequest fields + XCTAssertTrue(headersFromRequest.contains { $0.key == "Testname-1" && $0.value == "testvalue-1" }) + XCTAssertTrue(headersFromRequest.contains { $0.key == "Testname-2" && $0.value == "testvalue-2" }) + let expectedBody = try await httpBody.readData() + XCTAssertEqual(urlRequest.httpBody, expectedBody) + XCTAssertEqual(urlRequest.url, endpoint.url) + XCTAssertEqual(urlRequest.httpMethod, mockHttpRequest.method.rawValue) + } + func testCRTHeadersToSdkHeaders() throws { let builder = SdkHttpRequestBuilder() .withHeader(name: "Host", value: "amazon.aws.com") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ImportDeclarations.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ImportDeclarations.kt index da05a1b2f..eb305b223 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ImportDeclarations.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ImportDeclarations.kt @@ -8,7 +8,12 @@ package software.amazon.smithy.swift.codegen class ImportDeclarations { private val imports = mutableSetOf() - fun addImport(packageName: String, isTestable: Boolean = false, internalSPIName: String? = null) { + fun addImport( + packageName: String, + isTestable: Boolean = false, + internalSPIName: String? = null, + importOnlyIfCanImport: Boolean = false + ) { val existingImport = imports.find { it.packageName == packageName } if (existingImport != null) { // Update isTestable to true if needed @@ -17,10 +22,12 @@ class ImportDeclarations { if (internalSPIName != null) { existingImport.internalSPINames.add(internalSPIName) } + // Update importOnlyIfCanImport to true if needed + existingImport.importOnlyIfCanImport = existingImport.importOnlyIfCanImport || importOnlyIfCanImport } else { // Otherwise, we have a new package to import, so add it val internalSPINames = listOf(internalSPIName).mapNotNull { it }.toMutableSet() - imports.add(ImportStatement(packageName, isTestable, internalSPINames)) + imports.add(ImportStatement(packageName, isTestable, internalSPINames, importOnlyIfCanImport)) } } @@ -36,7 +43,12 @@ class ImportDeclarations { } } -private data class ImportStatement(val packageName: String, var isTestable: Boolean, val internalSPINames: MutableSet) { +private data class ImportStatement( + val packageName: String, + var isTestable: Boolean, + val internalSPINames: MutableSet, + var importOnlyIfCanImport: Boolean +) { val statement: String get() { var import = "import $packageName" @@ -46,6 +58,14 @@ private data class ImportStatement(val packageName: String, var isTestable: Bool if (isTestable) { import = "@testable $import" } + if (importOnlyIfCanImport) { + import = + """ + #if canImport($packageName) + $import + #endif + """.trimIndent() + } return import } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt index 56f099200..232a517f9 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt @@ -101,8 +101,13 @@ class SwiftWriter(private val fullPackageName: String) : CodeWriter() { } } - fun addImport(packageName: String, isTestable: Boolean = false, internalSPIName: String? = null) { - imports.addImport(packageName, isTestable, internalSPIName) + fun addImport( + packageName: String, + isTestable: Boolean = false, + internalSPIName: String? = null, + importOnlyIfCanImport: Boolean = false + ) { + imports.addImport(packageName, isTestable, internalSPIName, importOnlyIfCanImport) } fun addFoundationImport() { From ab999a9f0c972adcb350beda3c630a35697f7e8e Mon Sep 17 00:00:00 2001 From: AWS SDK Swift Automation Date: Tue, 7 Nov 2023 17:39:51 +0000 Subject: [PATCH 18/37] chore: Updates version to 0.35.0 --- Package.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.version b/Package.version index 75544d772..4d8ac4d2e 100644 --- a/Package.version +++ b/Package.version @@ -1 +1 @@ -0.34.0 \ No newline at end of file +0.35.0 \ No newline at end of file From 8d057318614b6fe61dda00e6bc4e734c78ce2dc5 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 22 Nov 2023 14:20:50 -0500 Subject: [PATCH 19/37] fix: Add a header to operation doc comments (#621) --- .../smithy/swift/codegen/ServiceGenerator.kt | 2 + .../test/kotlin/ContentMd5MiddlewareTests.kt | 75 ++++++++++--------- .../HttpProtocolClientGeneratorTests.kt | 66 ++++++++-------- .../test/kotlin/IdempotencyTokenTraitTests.kt | 73 +++++++++--------- 4 files changed, 111 insertions(+), 105 deletions(-) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceGenerator.kt index 66384add0..d63c59531 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceGenerator.kt @@ -76,6 +76,8 @@ class ServiceGenerator( * Helper method for generating in-line documentation for operation */ private fun renderOperationDoc(model: Model, service: ServiceShape, op: OperationShape, writer: SwiftWriter) { + writer.writeDocs("Performs the \\`${op.id.name}\\` operation on the \\`${service.id.name}\\` service.") + writer.writeDocs("") writer.writeShapeDocs(op) writer.writeAvailableAttribute(model, op) diff --git a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt index 13eeeb1d2..a3c58ee7f 100644 --- a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt @@ -6,43 +6,44 @@ class ContentMd5MiddlewareTests { fun `generates ContentMD5 middleware`() { val context = setupTests("Isolated/contentmd5checksum.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/RestXmlProtocolClient.swift") - val expectedContents = - """ - extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { - /// This is a very cool operation. - /// - /// - Parameter IdempotencyTokenWithStructureInput : [no documentation found] - /// - /// - Returns: `IdempotencyTokenWithStructureOutput` : [no documentation found] - public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput - { - let context = ClientRuntime.HttpContextBuilder() - .withEncoder(value: encoder) - .withDecoder(value: decoder) - .withMethod(value: .put) - .withServiceName(value: serviceName) - .withOperation(value: "idempotencyTokenWithStructure") - .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) - .withLogger(value: config.logger) - .withPartitionID(value: config.partitionID) - .build() - var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.buildStep.intercept(position: .before, middleware: ClientRuntime.ContentMD5Middleware()) - operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml")) - operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "IdempotencyToken")) - operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) - let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) - return result - } - - } - """.trimIndent() + val expectedContents = """ +extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { + /// Performs the `IdempotencyTokenWithStructure` operation on the `RestXml` service. + /// + /// This is a very cool operation. + /// + /// - Parameter IdempotencyTokenWithStructureInput : [no documentation found] + /// + /// - Returns: `IdempotencyTokenWithStructureOutput` : [no documentation found] + public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .put) + .withServiceName(value: serviceName) + .withOperation(value: "idempotencyTokenWithStructure") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.ContentMD5Middleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "IdempotencyToken")) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + +} +""" contents.shouldContainOnlyOnce(expectedContents) } private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index cb578f9b3..5324726f4 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -106,38 +106,40 @@ class HttpProtocolClientGeneratorTests { val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() val expected = """ - extension RestJsonProtocolClient: RestJsonProtocolClientProtocol { - /// This is a very cool operation. - /// - /// - Parameter AllocateWidgetInput : [no documentation found] - /// - /// - Returns: `AllocateWidgetOutput` : [no documentation found] - public func allocateWidget(input: AllocateWidgetInput) async throws -> AllocateWidgetOutput - { - let context = ClientRuntime.HttpContextBuilder() - .withEncoder(value: encoder) - .withDecoder(value: decoder) - .withMethod(value: .post) - .withServiceName(value: serviceName) - .withOperation(value: "allocateWidget") - .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) - .withLogger(value: config.logger) - .withPartitionID(value: config.partitionID) - .build() - var operation = ClientRuntime.OperationStack(id: "allocateWidget") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.clientToken)) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "AllocateWidgetInput")) - operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) - let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) - return result - } - """.trimIndent() +extension RestJsonProtocolClient: RestJsonProtocolClientProtocol { + /// Performs the `AllocateWidget` operation on the `Example` service. + /// + /// This is a very cool operation. + /// + /// - Parameter AllocateWidgetInput : [no documentation found] + /// + /// - Returns: `AllocateWidgetOutput` : [no documentation found] + public func allocateWidget(input: AllocateWidgetInput) async throws -> AllocateWidgetOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "allocateWidget") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "allocateWidget") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.clientToken)) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "AllocateWidgetInput")) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } +""" contents.shouldContainOnlyOnce(expected) } diff --git a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt index bf6db1240..e514406fe 100644 --- a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt @@ -6,42 +6,43 @@ class IdempotencyTokenTraitTests { fun `generates idempotent middleware`() { val context = setupTests("Isolated/idempotencyToken.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/RestXmlProtocolClient.swift") - val expectedContents = - """ - extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { - /// This is a very cool operation. - /// - /// - Parameter IdempotencyTokenWithStructureInput : [no documentation found] - /// - /// - Returns: `IdempotencyTokenWithStructureOutput` : [no documentation found] - public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput - { - let context = ClientRuntime.HttpContextBuilder() - .withEncoder(value: encoder) - .withDecoder(value: decoder) - .withMethod(value: .put) - .withServiceName(value: serviceName) - .withOperation(value: "idempotencyTokenWithStructure") - .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) - .withLogger(value: config.logger) - .withPartitionID(value: config.partitionID) - .build() - var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml")) - operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "IdempotencyToken")) - operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) - let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) - return result - } - - } - """.trimIndent() + val expectedContents = """ +extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { + /// Performs the `IdempotencyTokenWithStructure` operation on the `RestXml` service. + /// + /// This is a very cool operation. + /// + /// - Parameter IdempotencyTokenWithStructureInput : [no documentation found] + /// + /// - Returns: `IdempotencyTokenWithStructureOutput` : [no documentation found] + public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .put) + .withServiceName(value: serviceName) + .withOperation(value: "idempotencyTokenWithStructure") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "IdempotencyToken")) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } + +} +""" contents.shouldContainOnlyOnce(expectedContents) } private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { From 2658d06cc555e5187d1e71c8cbafd8c5c3828583 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 27 Nov 2023 11:03:49 -0600 Subject: [PATCH 20/37] remove unnecessary TODOs (#622) --- .../Http/HttpClientConfiguration.swift | 1 - .../Networking/Http/StreamableHttpBody.swift | 1 - design/DESIGN.md | 1026 ----------------- design/README.md | 2 - .../swift/codegen/ShapeValueGenerator.kt | 6 - .../smithy/swift/codegen/SymbolVisitor.kt | 6 +- .../HttpResponseTraitQueryParams.kt | 1 - .../formurl/StructEncodeFormURLGenerator.kt | 1 - .../swift/codegen/model/AddOperationShapes.kt | 2 - .../MockHttpResponseBindingErrorGenerator.kt | 11 +- 10 files changed, 3 insertions(+), 1054 deletions(-) delete mode 100644 design/DESIGN.md diff --git a/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift b/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift index 2ba975596..68ad52f8b 100644 --- a/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift +++ b/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift @@ -7,7 +7,6 @@ public class HttpClientConfiguration { public var protocolType: ProtocolType // initialize with default headers public var defaultHeaders: Headers - // TODO: this file will change post AWS Service config design most likely. // add any other properties here you want to give the service operations // control over to be mapped to the Http Client diff --git a/Sources/ClientRuntime/Networking/Http/StreamableHttpBody.swift b/Sources/ClientRuntime/Networking/Http/StreamableHttpBody.swift index 640a442a4..dd9ec8b08 100644 --- a/Sources/ClientRuntime/Networking/Http/StreamableHttpBody.swift +++ b/Sources/ClientRuntime/Networking/Http/StreamableHttpBody.swift @@ -27,7 +27,6 @@ class StreamableHttpBody: IStreamable { position = .min } - /// TODO: simplify logger creation logger = SwiftLogger(label: "HttpContent") } diff --git a/design/DESIGN.md b/design/DESIGN.md deleted file mode 100644 index c20ebead7..000000000 --- a/design/DESIGN.md +++ /dev/null @@ -1,1026 +0,0 @@ -# Swift Smithy SDK - -## Core Spec - -Reference the Smithy [Core Spec](https://smithy.io/spec/core.html) - -### Identifiers and Naming -Swift keywords can be found [here](https://docs.swift.org/swift-book/ReferenceManual/LexicalStructure.html) under Keywords & Punctuation. You can use a reserved word as an identifier if you put backticks (`func`) before and after it. Keywords other than `inout`, `var`, and `let` can be used as parameter names in a function declaration or function call without being escaped with backticks. - -The list of reserved keywords that shouldn't be used as identifiers in Swift is: -* `Any` -* `#available` -* `associatedtype` -* `associativity` -* `as` -* `break` -* `case` -* `catch` -* `class` -* `#colorLiteral` -* `#column` -* `continue` -* `convenience` -* `deinit` -* `default` -* `defer` -* `didSet` -* `do` -* `dynamic` -* `enum` -* `extension` -* `else` -* `#else` -* `#elseif` -* `#endif` -* `#error` -* `fallthrough` -* `false` -* `#file` -* `#fileLiteral` -* `fileprivate` -* `final` -* `for` -* `func` -* `#function` -* `get` -* `guard` -* `indirect` -* `infix` -* `if` -* `#if` -* `#imageLiteral` -* `in` -* `is` -* `import` -* `init` -* `inout` -* `internal` -* `lazy` -* `left` -* `let` -* `#line` -* `mutating` -* `none` -* `nonmutating` -* `nil` -* `open` -* `operator` -* `optional` -* `override` -* `postfix` -* `private` -* `protocol` -* `Protocol` -* `public` -* `repeat` -* `rethrows` -* `return` -* `required` -* `right` -* `#selector` -* `self` -* `Self` -* `set` -* `#sourceLocation` -* `super` -* `static` -* `struct` -* `subscript` -* `switch` -* `this` -* `throw` -* `throws` -* `true` -* `try` -* `Type` -* `typealias` -* `unowned` -* `var` -* `#warning` -* `weak` -* `willSet` -* `where` -* `while` - -### Simple Shapes - - -|Smithy Type| Description | Swift Type -|-----------|-------------------------------------------------------------------|------------------------ -|blob | Uninterpreted binary data | Data -|boolean | Boolean value type | Bool -|string | UTF-8 encoded string | String -|byte | 8-bit signed integer ranging from -128 to 127 (inclusive) | Int8 -|short | 16-bit signed integer ranging from -32,768 to 32,767 (inclusive) | Int16 -|integer | 32-bit signed integer ranging from -2^31 to (2^31)-1 (inclusive) | Int -|long | 64-bit signed integer ranging from -2^63 to (2^63)-1 (inclusive) | Int -|float | Single precision IEEE-754 floating point number | Float -|double | Double precision IEEE-754 floating point number | Double -|bigInteger | Arbitrarily large signed integer | 3rd party libraries avail and apple wrote a prototype [here](https://github.com/apple/swift/blob/master/test/Prototypes/BigInt.swift) -|bigDecimal | Arbitrary precision signed decimal number | possibly `Decimal` can be used here -|timestamp | Represents an instant in time with no UTC offset or timezone. The serialization of a timestamp is determined by a protocol. | Date -|document | Unstable Represents an untyped JSON-like value that can take on one of the following types: null, boolean, string, byte, short, integer, long, float, double, an array of these types, or a map of these types where the key is string. | Custom type provided by client runtime - -**QUESTION**: We should support the `document` type but perhaps we can wait until it's marked stable to do anything with it? At the very least we should annotate the type as unstable if it's going to be in a public API - - -#### `document` Type - -The `document` type is an untyped JSON-like value that can take on the following types: null, boolean, string, byte, short, integer, long, float, double, an array of these types, or a map of these types where the key is string. - - -This type is best represented as an enum type. Here we would use the JSONValue type created in the Amplify project which is an enum with an associated type and an extension on that enum that uses the Codable protocol for serialization/deserialization. - -```swift - - -/** - * Class representing a Smithy Document type. - * Can be a [SmithyNumber], [SmithyBool], [SmithyString], [SmithyNull], [SmithyArray], or [SmithyMap] - */ - -/// A utility type that allows us to represent an arbitrary JSON structure -public enum JSONValue { - case array([JSONValue]) - case boolean(Bool) - case number(Double) - case object([String: JSONValue]) - case string(String) - case null -} - -extension JSONValue: Codable { - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - if let value = try? container.decode([String: JSONValue].self) { - self = .object(value) - } else if let value = try? container.decode([JSONValue].self) { - self = .array(value) - } else if let value = try? container.decode(Double.self) { - self = .number(value) - } else if let value = try? container.decode(Bool.self) { - self = .boolean(value) - } else if let value = try? container.decode(String.self) { - self = .string(value) - } else { - self = .null - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - switch self { - case .array(let value): - try container.encode(value) - case .boolean(let value): - try container.encode(value) - case .number(let value): - try container.encode(value) - case .object(let value): - try container.encode(value) - case .string(let value): - try container.encode(value) - case .null: - try container.encodeNil() - } - } - -} - -extension JSONValue: Equatable { } - -extension JSONValue: ExpressibleByArrayLiteral { - public init(arrayLiteral elements: JSONValue...) { - self = .array(elements) - } -} - -extension JSONValue: ExpressibleByBooleanLiteral { - public init(booleanLiteral value: Bool) { - self = .boolean(value) - } -} - -extension JSONValue: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, JSONValue)...) { - let dictionary = elements.reduce([String: JSONValue]()) { acc, curr in - var newValue = acc - newValue[curr.0] = curr.1 - return newValue - } - self = .object(dictionary) - } -} - -extension JSONValue: ExpressibleByFloatLiteral { - public init(floatLiteral value: Double) { - self = .number(value) - } -} - -extension JSONValue: ExpressibleByIntegerLiteral { - public init(integerLiteral value: Int) { - self = .number(Double(value)) - } -} - -extension JSONValue: ExpressibleByNilLiteral { - public init(nilLiteral: ()) { - self = .null - } -} - -extension JSONValue: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { - self = .string(value) - } -} - -//extension to use subscribts to get the values from objects/arrays as normal -public extension JSONValue { - - subscript(_ key: String) -> JSONValue? { - guard case .object(let object) = self else { - return nil - } - return object[key] - } - - subscript(_ key: Int) -> JSONValue? { - switch self { - case .array(let array): - return array[key] - case .object(let object): - return object["\(key)"] - default: - return nil - } - } -} - -``` - -Example usage of building a doc or processing one - -```swift -func foo() { - let sourceString = #"{"stringValue": "a string", "numberValue": 123.45, "booleanValue": true}"# - let json = processDoc(source: sourceString) - /** prints an object that looks like below: - [ - "booleanValue": true, - "numberValue": 123.45, - "stringValue": "a string" - ] **/ - print(json) -} - -func processDoc(source: String) -> JSONValue { - let decoder = JSONDecoder() - let sourceData = source.data(using: .utf8) - let decodedObject = try decoder.decode(JSONValue.self, from: sourceData!) - -} - -``` - -### Aggregate types - - -| Smithy Type | Kotlin Type -|-------------|------------- -| list | Array -| set | Set -| map | Dictionary -| structure | struct or class ** -| union | enum - - -#### Structure - -A [structure](https://smithy.io/spec/core.html#structure) type represents a fixed set of named heterogeneous members. In Swift this can be represented -as either a struct or a class. - -Non boxed member values will be defaulted according to the spec: `The default value of a byte, short, integer, long, float, and double shape that is not boxed is zero` - -``` -list MyList { - member: String -} - -structure Foo { - bar: String, - baz: Integer, - quux: MyList -} -``` - -##### ALTERNATIVE 1 - -```swift -class Foo { - let bar: String? - let baz: Int - let quux: List? - - init(bar: String? = nil, baz: Int = 0, quux: List? = nil) { - self.bar = bar - self.baz = baz - self.quux = quux - } -} -``` - -##### ALTERNATIVE 2 - -```swift -struct Foo{ - let bar: String? - let baz = 0 - let quux: List? -} -``` - -Usually in Swift we usually go with structs for several reasons. You get a default constructor out of the box with structs that you do not need to declare and they are value types not reference types. Because classes are reference types, it’s possible for multiple constants and variables to refer to the same single instance of a class behind the scenes which is something to think about. - -Structs and classes in Swift both: -- Define properties to store values -- Define methods to provide functionality -- Define subscripts to provide access to their values using subscript syntax -- Define initializers to set up their initial state -- Be extended to expand their functionality beyond a default implementation -- Conform to protocols to provide standard functionality of a certain kind - -Classes can do a few other things: -- Inheritance enables one class to inherit the characteristics of another. (but you can only inherit one class whereas you can inherit multiple protocols so class inheritance with a class hierachy isn't used often in Swift) -- Type casting enables you to check and interpret the type of a class instance at runtime. -- Deinitializers enable an instance of a class to free up any resources it has assigned. -- Reference counting allows more than one reference to a class instance. (this is automatic using ARC which stands for Automatic reference counting. refernce to instance isn't removed until all references are removed) - - -#### Union - -A [union](https://smithy.io/spec/core.html#union) is a fixed set of types where only one type is used at any one time. In Swift this maps well to a [enum](https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html). Enums in swift are like enums on steroids, they can have associated types, raw values, and recursive enums. - -Example - -``` -# smithy - -union MyUnion { - bar: Integer, - foo: String -} -``` - -```swift -enum MyUnion { - struct Bar { - public let bar: Int - } - struct Foo { - public let foo: String - } - case bar(Bar) - case foo(Foo) -} - -``` - - -### Service types - -Services will generate both an interface as well as a concrete client implementation. - -Each operation will generate a method with the given operation name and the `input` and `output` shapes of that operation. - - -The following example from the Smithy quickstart has been abbreviated. All input/output operation structure bodies have been omitted as they aren't important to how a service is defined. - -``` -service Weather { - version: "2006-03-01", - resources: [City], - operations: [GetCurrentTime] -} - -@readonly -operation GetCurrentTime { - output: GetCurrentTimeOutput -} - -structure GetCurrentTimeOutput { - @required - time: Timestamp -} - -resource City { - identifiers: { cityId: CityId }, - read: GetCity, - list: ListCities, - resources: [Forecast], -} - -resource Forecast { - identifiers: { cityId: CityId }, - read: GetForecast, -} - -// "pattern" is a trait. -@pattern("^[A-Za-z0-9 ]+$") -string CityId - -@readonly -operation GetCity { - input: GetCityInput, - output: GetCityOutput, - errors: [NoSuchResource] -} - -structure GetCityInput { ... } - -structure GetCityOutput { ... } - -@error("client") -structure NoSuchResource { ... } - -@readonly -@paginated(items: "items") -operation ListCities { - input: ListCitiesInput, - output: ListCitiesOutput -} - -structure ListCitiesInput { ... } - -structure ListCitiesOutput { ... } - -@readonly -operation GetForecast { - input: GetForecastInput, - output: GetForecastOutput -} - -structure GetForecastInput { ... } -structure GetForecastOutput { ... } -``` - - -```swift -protocol Weather { - - typealias GetCurrentTimeOutputCompletion = (GetCurrentTimeOutput) -> Void - - typealias GetCityOutputCompletion = (GetCityOutput) -> Void - - typealias ListCitiesOutputCompletion = (ListCitiesOutput) -> Void - - typealias GetForecastOutputCompletion = (GetForecaseOutput) -> Void - - func getCurrentTime(completion: @escaping GetCurrentTimeOutputCompletion) - - func getCity(input: GetCityInput, completion: @escaping GetCityOutputCompletion) - - func listCities(input: ListCitiesInput, completion: @escaping ListCitiesOutputCompletion) - - func getForecast(input: GetForecastInput, completion: @escaping GetForecastOutputCompletion) -} - -class WeatherClient : Weather { - - func getCurrentTime(completion: @escaping GetCurrentTimeOutputCompletion) { - ... - let result = //calls to server - completion(result) - } - - func getCity(input: GetCityInput, completion: @escaping GetCityOutputCompletion) { - ... - let result = //calls to server - completion(result) - } - - func listCities(input: ListCitiesInput, completion: @escaping ListCitiesOutputCompletion) { - ... - } - - func getForecast(input: GetForecastInput, completion: @escaping GetForecastOutputCompletion){ - ... - } -} - -``` - -#### Considerations - -1. Closures. Closures in swift are how we can represent async operations. - -All service operations are expected to be async operations under the hood since they imply a network call. Making this explicit in the interface with closures sets expectations up front. - - -2. OperationQueues. If we build the client runtime package in swift we can do something similar to Amplify with establishing an Asynchronous Operation and adding service requests to the queue. If we go with Kotlin MPP, not sure how this will work for iOS? - - -3. Backwards Compatibility. Slight breaking change as completion handlers in current sdk are after you call `continueWith` - -```swift -//old way -let service = FooService() -service.getForecast(input: GetForecaseInput()).continueWith { (task) -> Any? in - -} - -//new way -let service = FooService() -service.getForecast(input: GetForecaseInput()){ output in - -} -``` - -### Resource types - -Each resources will be processed for each of the corresponding lifecycle operations as well as the non-lifecycle operations. - -Every operation, both lifecycle and non-lifecycle, will generate a method on the service class to which the resource belongs. - -This will happen recursively since resources can have child resources. - -See the Service section which has a detailed example of how resources show up on a service. - - -### Traits - -### Type Refinement Traits - -#### `box` trait - -Indicates that a shape is boxed which means the member may or may not contain a value and that the member has no default value. We would use optionals in Swift to represent this. - -NOTE: all shapes other than primitives are always considered boxed in the Smithy spec - -``` -structure Foo { - @box - bar: integer - -} -``` - - -```swift -struct Foo { - let bar: Int? -} -``` - -**QUESTION**: If all non-primitive types (e.g. String, Structure, List, etc) are considered boxed should they all be generated as nullable in Kotlin? -e.g. - -``` -structure Baz { - quux: integer -} - -structure Foo { - bar: String, - baz: Baz -} - -``` - -```swift -struct Baz{ - let quux = 0 -} - -struct Foo { - let bar: String? - let baz: Baz? -} - -``` - - -#### `deprecated` trait - -Will generate the equivalent code for the shape annotated with Swifts's `@available` attribute and pass in the deprecated and message arguments with the version and message if available or just pass the argument name without the colon and value as demonstrated below. - -``` -@deprecated -structure Foo - -@deprecated(message: "no longer used", since: "1.3") -structure Bar -``` - -```swift -@available(deprecated) -class Foo {} -``` - -```swift -@available(deprecated: 1.3, message: "no longer used") -class Bar {} - -``` - -#### `error` trait - -The `error` trait will be processed as an exception type in Swift. This requires support from the client-runtime lib. See "Exceptions" in the Appendix. - - -Note the Smithy core spec indicates: `The message member of an error structure is special-cased. It contains the human-readable message that describes the error. If the message member is not defined in the structure, code generated for the error may not provide an idiomatic way to access the error message (e.g., an exception message in Java).` - -If present these should be translated to the `ServiceException::errorMessage` property. - - -The `httpError` trait should not need additional processing assuming the HTTP response itself is exposed in someway on `ServiceException`. - - -``` -@error("server") -@httpError(501) -structure NotImplemented {} - -@error("client") -@retryable -structure ThrottlingError { - @required - message: String, -} - -``` - - -```swift - -enum ErrorType { - case server - case client -} - -enum ServiceException : Error { - case notImplementedException(NotImplementedException) - case throttlingError(ThrottlingError) -} - -struct NotImplementedException: Exception { - public let isRetryable = false - public let errorType = .server - public let serviceName = "MyService" -} - -struct ThrottlingError : Exception { - public let isRetryable = true - public let errorType = .client - public let serviceName = "MyService" -} - -protocol Exception { - public let message: String - public let serviceName: String - public let errorType: ErrorType - public let isRetryable: Bool -} - -``` - - -### Constraint traits - -#### `enum` trait - -Swift has first class support for enums and the SDK should make use of them to provide a type safe interface. - -When no `name` is provided the enum name will be the same as the value albeit lowercased (as this is idiomatic), otherwise the Swift SDK will use the provided enum name. - - -``` -@enum("YES": {}, "NO": {}) -string SimpleYesNo - -@enum("Yes": {name: "YES"}, "No": {name: "NO"}) -string TypedYesNo -``` - -```swift -enum SimpleYesNo : String { - case yes = "yes" - case no = "no" - -} -enum TypedYesNo : String { - case yes = "yes" - case no = "no" -} -``` - - -``` -@enum( - t2.nano: { - name: "T2_NANO", - documentation: """ - T2 instances are Burstable Performance - Instances that provide a baseline level of CPU - performance with the ability to burst above the - baseline.""", - tags: ["ebsOnly"] - }, - t2.micro: { - name: "T2_MICRO", - documentation: """ - T2 instances are Burstable Performance - Instances that provide a baseline level of CPU - performance with the ability to burst above the - baseline.""", - tags: ["ebsOnly"] - }, - m256.mega: { - name: "M256_MEGA", - deprecated: true - } -) -string MyString -``` - - -```swift -enum MyString : String { - - /** - * T2 instances are Burstable Performance Instances that provide a baseline level of CPU performance with the ability to burst above the baseline. - */ - case t2_nano = "t2.nano" - - /** - * T2 instances are Burstable Performance Instances that provide a baseline level of CPU performance with the ability to burst above the baseline. - */ - case t2_micro = "t2.micro" - - @available(deprecated: 1.3) - case m256.mega = "m256.mega" - -} -``` - -#### Considerations - -**Deprecation** - -Concern here is that with deprecating something in Swift you need to provide the version as the value of the argument and looks like here it isn't provided in the Smithy enum model. How do we account for that? - -**Unknown Enum Names** - -The Smithy core spec indicates that unknown enum values need to be handled as well. - -``` -Consumers that choose to represent enums as constants SHOULD ensure that unknown enum names returned from a service do not cause runtime failures. -``` - -This is fine for swift because we can use the @unknown attribute in a swift statement to handle it and not fail like this: -```swift -switch string { -case .t2_micro: - //do something here -case .t2_nano: - //do something here -@unknown default: - print("unknown value") -} -``` - -In terms of deserialization of unknown enum values we need to do a litle extra work and there are alternatives to what that is - -**ALTERNATIVE 1** - -use custom encoding when deserializing -```swift -enum Material: String, Codable { - case wood, metal, glass, unknown -} - -extension Material { - init(from decoder: Decoder) throws { - self = try Material(from: decoder, default: .unknown) - } -} - -//here we are mapping unknown values to the .other case - -extension RawRepresentable where RawValue: Decodable { - init(from decoder: Decoder, default: Self) throws { - let container = try decoder.singleValueContainer() - let rawValue = try container.decode(RawValue.self) - self = Self(rawValue: rawValue) ?? `default` - } -} -//If a RawRepresentable’s RawValue type is decodable, we’re offering an initialiser that tries to decode a raw value of that type. If that raw value does not match a represented value, it will fallback to a provided default. - -//then we can change extension -``` - -**ALTERNATIVE 2** - -We can use CaseIterable here to loop through all the cases and see if it matches our raw value given and if not we can provide an unknonw option. This allows us to handle it not just at the decoding level but also at the instantiation level like this -```swift -protocol UnknownCaseRepresentable: RawRepresentable, CaseIterable where RawValue: Equatable { - static var unknownCase: Self { get } -} - -extension UnknownCaseRepresentable { - init(rawValue: RawValue) { - let value = Self.allCases.first(where: { $0.rawValue == rawValue }) - self = value ?? Self.unknownCase - } -} - -enum Material: String { - case wood, metal, glass, unknown -} - -extension Material: Codable {} - -extension Material: UnknownCaseRepresentable { - static let unknownCase: Material = .unknown -} - -//then when you instantiate like this -Material(rawValue: "stone") // -> .unknown -``` - I think alternative 2 might be the best option here to capture both the deserialization of the unknown values in enums in smithy and also the instantiation of them but not sure what is important here for these unknown values. The question is what is happening to these enums with unknown values being returned from a service? Are we just deserializing them? Are we taking some action that needs to be handled first? - - -#### `idRef` trait -Not processed - -#### `length` trait -**TODO** -**QUESTION** I don't even see where these constraints (length, range, pattern, etc) are processed in the smithy-typescript/smithy-go code generators. Are they not implemented? - -#### `pattern` trait -**TODO** - -#### `private` trait -Not processed - -#### `range` trait -**TODO** - -#### `required` trait - -``` -struct Foo { - @required - bar: String -} -``` - -All members marked `required` should show up in the class as nonoptional - -```swift -struct Foo { - let bar: String -} -``` - -#### `uniqueItems` trait -**TODO** - -### Behavior traits - -#### `idempotentcyToken` trait -**TODO** The spec states that `clients MAY automatically provide a value`. This could be interpreted to provide a default UUID and allow it to be overridden. - -#### `idempotent` trait - -Not processed - -**FUTURE** It may be worthwhile generating documentation that indicates the operation is idempotent. - -#### `readonly` trait - -Not processed - -#### `retryable` trait - -This trait influences errors, see the `error` trait for how it will be handled. - - -#### `paginated` trait - -Not processed - -### Resource traits - -#### `references` trait -Not processed - -#### `resourceIdentifier` trait -Not processed - -### Protocol traits - -#### `protocols` trait - -Inspected to see if the protocol is supported by the code generator/client-runtime. If no protocol is supported codegen will fail. - -The `auth` peroperty of this trait will be inspected just to confirm at least one of the authentication schemes is supported. - -All of the built-in HTTP authentication schemes will be supported by being able to customize the request headers. - - -#### `auth` trait - -Processed the same as the `auth` property of the `protocols` trait. - -#### `jsonName` trait - -The generated class member will have the `@SerialName("...")` annotation added to the property. - -This will create an enum in the class with its coding keys like below: - -```swift -struct Employee: Codable { - public let barFoo: Int - public let fooBar: String - - enum CodingKeys: String, CodingKey { - case barFoo = "bar_foo" - case fooBar = "foo_bar" - } - //the coding keys represent the keys= names in json -} -``` - -#### `mediaType` trait - -The media type trait SHOULD influence the HTTP Content-Type header if not already set. - -#### `timestampFormat` trait - -We will use the `Date` type in swift. I presume we will need some Date extensions to handle various date formats. - -### Documentation traits - -#### `documentation` trait - -All top level classes, enums, and their members will be generated with the given documentation. - -This will use the 3 slashes or /** syntax for block documentation like so: - -```swift -///this is documentation -/** this is also documentation */ -func fooService() { - -} -``` -#### `examples` trait - -Not processed - -**FUTURE** We probably should process this but I think it's ok to put it lower priority - -#### `externalDocumentation` trait - -Processed the same as the `documentation` trait. The link will be processed appropriately for the target documentation engine (e.g. [dokka](https://github.com/Kotlin/dokka)). - -#### `sensitive` trait - -Not processed - -#### `since` trait - -Not processed - -**FUTURE** We should probably process this into the generated documentation at least. - -#### `tags` trait - -Not processed - -#### `title` trait - -Combined with the generated documentation as the first text to show up for a service or resource. - - -### Endpoint traits - -#### `endpoint` trait -**TODO** - -#### `hostLabel` trait -**TODO** - - -# HTTP Protocol Bindings - -**TODO** - - -# Appendix - diff --git a/design/README.md b/design/README.md index 7e7163969..3f483a868 100644 --- a/design/README.md +++ b/design/README.md @@ -1,7 +1,5 @@ # smithy-swift Design Documents -### Legacy docs: available [here](DESIGN.md) - ### Design template: available [here](rfc-00000-template.md) Designs are generally conceived, written and reviewed internally. diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt index a82b13d2b..6f99dc769 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt @@ -113,7 +113,6 @@ class ShapeValueGenerator( .indent() .call { block() } .dedent() - // TODO:: fix indentation when `writeInline` retains indent .writeInline("\n)") if (recursiveMemberWithTrait) { @@ -336,11 +335,6 @@ class ShapeValueGenerator( ShapeType.BYTE, ShapeType.SHORT, ShapeType.INTEGER, ShapeType.LONG, ShapeType.DOUBLE, ShapeType.FLOAT -> writer.writeInline("\$L", node.value) - /* - TODO:: When https://github.com/apple/swift-numerics supports Integer conforming to Real protocol, - we need to change "Array(String($L).utf8)" to Complex. Apple's work is being - tracked in apple/swift-numerics#5 - */ ShapeType.BIG_INTEGER -> { writer.addImport(SwiftDependency.BIG.target) writer.writeInline("Array(String(\$L).utf8)", node.value) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SymbolVisitor.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SymbolVisitor.kt index e7f80eb9d..b337aed89 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SymbolVisitor.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SymbolVisitor.kt @@ -115,10 +115,6 @@ class SymbolVisitor(private val model: Model, swiftSettings: SwiftSettings) : override fun shortShape(shape: ShortShape): Symbol = numberShape(shape, "Int16", "0") - /* - TODO:: When https://github.com/apple/swift-numerics supports Integer conforming to Real protocol, we need to - change [UInt8] to Complex. Apple's work is being tracked in apple/swift-numerics#5 - */ override fun bigIntegerShape(shape: BigIntegerShape): Symbol = createBigSymbol(shape, "[UInt8]") override fun bigDecimalShape(shape: BigDecimalShape): Symbol = createBigSymbol(shape, "Complex") @@ -197,7 +193,7 @@ class SymbolVisitor(private val model: Model, swiftSettings: SwiftSettings) : } override fun resourceShape(shape: ResourceShape): Symbol { - // TODO create resource type + // May implement a resource type in future return createSymbolBuilder(shape, "Any", true).build() } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitQueryParams.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitQueryParams.kt index eda0417ae..c90853df5 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitQueryParams.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitQueryParams.kt @@ -16,7 +16,6 @@ class HttpResponseTraitQueryParams( val responseBindings: List, val writer: SwiftWriter ) { - // TODO: Support proper deserialization of http response query fun render() { val bodyMembers = responseBindings.filter { it.location == HttpBinding.Location.DOCUMENT } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/formurl/StructEncodeFormURLGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/formurl/StructEncodeFormURLGenerator.kt index e0d445ca9..7422b6b11 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/formurl/StructEncodeFormURLGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/formurl/StructEncodeFormURLGenerator.kt @@ -67,7 +67,6 @@ class StructEncodeFormURLGenerator( } } - // TODO: Make this pluggable so that this code can exist in aws-sdk-swift private fun addConstantMembers(containerName: String) { if (shapeMetadata.containsKey(ShapeMetadata.OPERATION_SHAPE) && shapeMetadata.containsKey(ShapeMetadata.SERVICE_VERSION)) { val operationShape = shapeMetadata[ShapeMetadata.OPERATION_SHAPE] as OperationShape diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/AddOperationShapes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/AddOperationShapes.kt index 611713696..991c0e0fa 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/AddOperationShapes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/AddOperationShapes.kt @@ -39,8 +39,6 @@ class AddOperationShapes { for (operation in operations) { val operationId = operation.id LOGGER.info("building unique input/output shapes for $operationId") - // TODO: MUST FIX BEFORE SHIPPING. check to see if new synthetic input or output shapes conflict with any other shapes - // in the model by walking the model and fail code generation val inputShape = operation.input .map { shapeId -> cloneOperationShape( diff --git a/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpResponseBindingErrorGenerator.kt b/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpResponseBindingErrorGenerator.kt index 72fb829c0..aba1cb16a 100644 --- a/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpResponseBindingErrorGenerator.kt +++ b/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpResponseBindingErrorGenerator.kt @@ -36,14 +36,7 @@ class MockHttpResponseBindingErrorGenerator : HttpResponseBindingErrorGeneratabl } override fun renderServiceError(ctx: ProtocolGenerator.GenerationContext) { - /* - - TODO( - "Organize test suites of smithy-swift and aws-sdk-swift " + - "and see if this class and consumers of this class should be moved to aws-sdk-swift " + - "OR AWS protocol tests in aws-sdk-swift should be moved to smithy-swift." - ) - - */ + // not yet implemented + return } } From b1a0cf4da6defafde3f043ebd03a105a899e88ce Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 28 Nov 2023 12:42:59 -0600 Subject: [PATCH 21/37] fix: Codegen issues re: recursion, Swift keywords in unions (#623) --- .../smithy/swift/codegen/CodegenVisitor.kt | 2 + .../smithy/swift/codegen/SymbolVisitor.kt | 2 +- .../smithy/swift/codegen/UnionGenerator.kt | 21 +++--- .../customtraits/RecursiveUnionTrait.kt | 12 ++++ .../serde/json/MemberShapeDecodeGenerator.kt | 20 ++++-- .../codegen/model/UnionIndirectivizer.kt | 65 +++++++++++++++++++ .../kotlin/StructDecodeGenerationTests.kt | 18 +++-- .../src/test/kotlin/UnionGeneratorTests.kt | 9 ++- 8 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/customtraits/RecursiveUnionTrait.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/UnionIndirectivizer.kt diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt index 5ee3b05e0..063475c90 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt @@ -27,6 +27,7 @@ import software.amazon.smithy.swift.codegen.integration.SwiftIntegration import software.amazon.smithy.swift.codegen.model.AddOperationShapes import software.amazon.smithy.swift.codegen.model.NestedShapeTransformer import software.amazon.smithy.swift.codegen.model.RecursiveShapeBoxer +import software.amazon.smithy.swift.codegen.model.UnionIndirectivizer import software.amazon.smithy.swift.codegen.model.hasTrait import java.util.ServiceLoader import java.util.logging.Logger @@ -88,6 +89,7 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default() { resolvedModel = AddOperationShapes.execute(resolvedModel, settings.getService(resolvedModel), settings.moduleName) resolvedModel = RecursiveShapeBoxer.transform(resolvedModel) resolvedModel = NestedShapeTransformer.transform(resolvedModel, settings.getService(resolvedModel)) + resolvedModel = UnionIndirectivizer.transform(resolvedModel) return resolvedModel } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SymbolVisitor.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SymbolVisitor.kt index b337aed89..f8911ba5d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SymbolVisitor.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SymbolVisitor.kt @@ -97,7 +97,7 @@ class SymbolVisitor(private val model: Model, swiftSettings: SwiftSettings) : override fun toMemberName(shape: MemberShape): String { val containingShape = model.expectShape(shape.container) if (containingShape is UnionShape) { - val name = escaper.escapeMemberName(shape.memberName) + val name = escaper.escapeMemberName(shape.memberName.toLowerCamelCase()) return if (!name.equals("sdkUnknown")) lowerCase(name) else name } return escaper.escapeMemberName(shape.memberName.toLowerCamelCase()) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/UnionGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/UnionGenerator.kt index e7adcceb1..eeb2c2b11 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/UnionGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/UnionGenerator.kt @@ -8,9 +8,11 @@ package software.amazon.smithy.swift.codegen import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.swift.codegen.customtraits.NestedTrait +import software.amazon.smithy.swift.codegen.customtraits.RecursiveUnionTrait import software.amazon.smithy.swift.codegen.model.eventStreamEvents import software.amazon.smithy.swift.codegen.model.expectShape import software.amazon.smithy.swift.codegen.model.hasTrait @@ -71,28 +73,21 @@ class UnionGenerator( fun renderUnion() { writer.writeShapeDocs(shape) writer.writeAvailableAttribute(model, shape) - val indirectKeywordIfNeeded = if (needsIndirectKeyword(unionSymbol.name, shape)) "indirect " else "" - writer.openBlock("public ${indirectKeywordIfNeeded}enum \$union.name:L: \$N {", "}\n", SwiftTypes.Protocols.Equatable) { + val indirectOrNot = "indirect ".takeIf { shape.hasTrait() } ?: "" + writer.openBlock("public ${indirectOrNot}enum \$union.name:L: \$N {", "}\n", SwiftTypes.Protocols.Equatable) { // event streams (@streaming union) MAY have variants that target errors. // These errors if encountered on the stream will be thrown as an exception rather // than showing up as one of the possible events the consumer will see on the stream (AsyncThrowingStream). val members = shape.eventStreamEvents(model) - members.forEach { - writer.writeMemberDocs(model, it) - val enumCaseName = symbolProvider.toMemberName(it) - val enumCaseAssociatedType = symbolProvider.toSymbol(it) + members.forEach { member: MemberShape -> + writer.writeMemberDocs(model, member) + val enumCaseName = symbolProvider.toMemberName(member) + val enumCaseAssociatedType = symbolProvider.toSymbol(member) writer.write("case \$L(\$L)", enumCaseName, enumCaseAssociatedType) } // add the sdkUnknown case which will always be last writer.write("case sdkUnknown(\$N)", SwiftTypes.String) } } - - private fun needsIndirectKeyword(unionSymbolName: String, shape: UnionShape): Boolean { - val membersReferencingUnion = shape.allMembers.values.filter { - (symbolProvider.toSymbol(it).name).equals(unionSymbolName) - } - return membersReferencingUnion.isNotEmpty() - } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/customtraits/RecursiveUnionTrait.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/customtraits/RecursiveUnionTrait.kt new file mode 100644 index 000000000..3dc61f216 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/customtraits/RecursiveUnionTrait.kt @@ -0,0 +1,12 @@ +package software.amazon.smithy.swift.codegen.customtraits + +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.traits.Trait + +class RecursiveUnionTrait : Trait { + val ID = ShapeId.from("software.amazon.smithy.swift.codegen.swift.synthetic#RecursiveUnion") + override fun toNode(): Node = Node.objectNode() + + override fun toShapeId(): ShapeId = ID +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeDecodeGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeDecodeGenerator.kt index 8695ff912..ffcb75de1 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeDecodeGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeDecodeGenerator.kt @@ -179,11 +179,21 @@ abstract class MemberShapeDecodeGenerator( } else { // decode date as a string manually val dateName = "date$level" val swiftTimestampName = TimestampHelpers.generateTimestampFormatEnumValue(timestampFormat) - writer.write( - "let \$L = try containerValues.timestampStringAsDate(\$L, format: .\$L, forKey: .\$L)", - dateName, iteratorName, swiftTimestampName, topLevelMember.memberName - ) - writer.write("${decodedMemberName}$terminator.$insertMethod($dateName)") + if (!isSparse) { + writer.openBlock("if let $iteratorName = $iteratorName {", "}") { + writer.write( + "let \$L = try containerValues.timestampStringAsDate(\$L, format: .\$L, forKey: .\$L)", + dateName, iteratorName, swiftTimestampName, topLevelMember.memberName + ) + writer.write("${decodedMemberName}$terminator.$insertMethod($dateName)") + } + } else { + writer.write( + "let \$L = try containerValues.timestampStringAsDate(\$L, format: .\$L, forKey: .\$L)", + dateName, iteratorName, swiftTimestampName, topLevelMember.memberName + ) + writer.write("${decodedMemberName}$terminator.$insertMethod($dateName)") + } } } is CollectionShape -> { diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/UnionIndirectivizer.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/UnionIndirectivizer.kt new file mode 100644 index 000000000..b1449b644 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/UnionIndirectivizer.kt @@ -0,0 +1,65 @@ +package software.amazon.smithy.swift.codegen.model + +import software.amazon.smithy.codegen.core.TopologicalIndex +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.selector.PathFinder +import software.amazon.smithy.model.shapes.ListShape +import software.amazon.smithy.model.shapes.MapShape +import software.amazon.smithy.model.shapes.SetShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.model.transform.ModelTransformer +import software.amazon.smithy.swift.codegen.customtraits.RecursiveUnionTrait +import software.amazon.smithy.swift.codegen.customtraits.SwiftBoxTrait + +object UnionIndirectivizer { + fun transform(model: Model): Model { + // Transform the model, adding one `RecursiveTrait` at a time, until no more unmarked recursive unions remain. + val next = transformInner(model) + return if (next == null) { + model + } else { + transform(next) + } + } + + // Find the recursive unions, then mark one as recursive. Only mark one, then + // return so the model can be re-evaluated. + private fun transformInner(model: Model): Model? { + val index = TopologicalIndex(model) + val recursiveUnions = index.recursiveShapes.filter { it.isUnionShape } + val loops = recursiveUnions.map { shape: Shape -> + index.getRecursiveClosure(shape) + } + val loopToFix = loops.firstOrNull { loop: Set -> + !containsIndirection(loop.map { it.startShape }) + } + + return loopToFix?.let { loop: Set -> + check(loop.isNotEmpty()) + val unionPathToIndirectivize = loop.first { it.startShape.isUnionShape } + val unionShapeToIndirectivize = unionPathToIndirectivize.startShape.asUnionShape().get() + ModelTransformer.create().mapShapes(model) { shape -> + if (shape.id == unionShapeToIndirectivize.id) { + shape.asUnionShape().get().toBuilder().addTrait(RecursiveUnionTrait()).build() + } else { + shape + } + } + } + } + + private fun containsIndirection(loop: List): Boolean { + return loop.find { + when { + it is ListShape || + it is MapShape || + it is UnionShape && it.hasTrait() || + it is StructureShape && it.hasTrait() || + it is SetShape -> true + else -> false + } + } != null + } +} diff --git a/smithy-swift-codegen/src/test/kotlin/StructDecodeGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/StructDecodeGenerationTests.kt index 161d1bbdd..8dcfc543e 100644 --- a/smithy-swift-codegen/src/test/kotlin/StructDecodeGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/StructDecodeGenerationTests.kt @@ -213,8 +213,10 @@ extension TimestampInputOutputBody: Swift.Decodable { if let list0 = list0 { list0Decoded0 = [Swift.String]() for timestamp1 in list0 { - let date1 = try containerValues.timestampStringAsDate(timestamp1, format: .dateTime, forKey: .nestedTimestampList) - list0Decoded0?.append(date1) + if let timestamp1 = timestamp1 { + let date1 = try containerValues.timestampStringAsDate(timestamp1, format: .dateTime, forKey: .nestedTimestampList) + list0Decoded0?.append(date1) + } } } if let list0Decoded0 = list0Decoded0 { @@ -228,8 +230,10 @@ extension TimestampInputOutputBody: Swift.Decodable { if let timestampListContainer = timestampListContainer { timestampListDecoded0 = [ClientRuntime.Date]() for timestamp0 in timestampListContainer { - let date0 = try containerValues.timestampStringAsDate(timestamp0, format: .dateTime, forKey: .timestampList) - timestampListDecoded0?.append(date0) + if let timestamp0 = timestamp0 { + let date0 = try containerValues.timestampStringAsDate(timestamp0, format: .dateTime, forKey: .timestampList) + timestampListDecoded0?.append(date0) + } } } timestampList = timestampListDecoded0 @@ -375,8 +379,10 @@ extension NestedShapesOutputBody: Swift.Decodable { if let timestamplist0 = timestamplist0 { timestamplist0Decoded0 = [Swift.String]() for timestamp1 in timestamplist0 { - let date1 = try containerValues.timestampStringAsDate(timestamp1, format: .dateTime, forKey: .nestedListInDict) - timestamplist0Decoded0?.append(date1) + if let timestamp1 = timestamp1 { + let date1 = try containerValues.timestampStringAsDate(timestamp1, format: .dateTime, forKey: .nestedListInDict) + timestamplist0Decoded0?.append(date1) + } } } nestedListInDictDecoded0?[key0] = timestamplist0Decoded0 diff --git a/smithy-swift-codegen/src/test/kotlin/UnionGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/UnionGeneratorTests.kt index da1c26426..123c36d3c 100644 --- a/smithy-swift-codegen/src/test/kotlin/UnionGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/UnionGeneratorTests.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.model.traits.DocumentationTrait import software.amazon.smithy.swift.codegen.SwiftCodegenPlugin import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.UnionGenerator +import software.amazon.smithy.swift.codegen.model.UnionIndirectivizer class UnionGeneratorTests { @@ -111,11 +112,13 @@ class UnionGeneratorTests { ) val simpleUnionShape = simpleUnionShapeBuilder.build() val model = createModelFromShapes(simpleUnionShape) - val settings = model.defaultSettings() - val provider: SymbolProvider = SwiftCodegenPlugin.createSymbolProvider(model, settings) + val transformedModel = UnionIndirectivizer.transform(model) + val settings = transformedModel.defaultSettings() + val provider: SymbolProvider = SwiftCodegenPlugin.createSymbolProvider(transformedModel, settings) val writer = SwiftWriter("MockPackage") + val transformedUnionShape = transformedModel.expectShape(simpleUnionShape.id).asUnionShape().get() - val generator = UnionGenerator(model, provider, writer, simpleUnionShape, settings) + val generator = UnionGenerator(transformedModel, provider, writer, transformedUnionShape, settings) generator.render() val contents = writer.toString() From 34aecef034058b1c4f32f68226f1f39f175fc8a9 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 28 Nov 2023 19:59:16 -0600 Subject: [PATCH 22/37] feat!: Replace the XML encoder with a custom Smithy implementation (#619) --- .swiftlint.yml | 2 + Package.swift | 41 +- .../DefaultMessageEncoderStream.swift | 18 +- .../RequestBody/BlobBodyMiddleware.swift | 35 ++ .../BlobStreamBodyMiddleware.swift | 37 ++ .../BodyMiddleware.swift} | 30 +- .../RequestBody/EnumBodyMiddleware.swift | 37 ++ .../EventStreamBodyMiddleware.swift | 60 +++ .../RequestBody/PayloadBodyMiddleware.swift | 59 +++ .../RequestBody/StringBodyMiddleware.swift | 35 ++ .../PrimitiveTypeExtensions/BoxType.swift | 31 -- .../PrimitiveTypeExtensions/Indirect.swift | 22 + .../Encoder/DynamicNodeEncoding.swift | 11 - .../Encoder/RequestEncoder.swift | 6 +- .../Encoder/XMLEncoder+Extensions.swift | 14 - .../FormURL/Encoder/FormURLReadWrite.swift | 41 ++ .../SerializationUtils/JSONReadWrite.swift | 40 ++ .../TimestampFormatter.swift | 11 + .../SerializationUtils/XML/MapEntry.swift | 2 +- .../SerializationUtils/XML/MapKeyValue.swift | 2 +- .../SerializationUtils/XML/ValueWriters.swift | 22 + .../DocumentWritingClosure.swift | 10 + Sources/SmithyReadWrite/WritingClosure.swift | 9 + .../RequestTestUtil/HttpRequestTestBase.swift | 11 +- .../DateFormatters.swift | 1 + .../TimestampSerdeUtils.swift | 5 +- Sources/SmithyXML/DocumentWriter.swift | 18 + Sources/SmithyXML/Writer/NodeInfo.swift | 34 ++ Sources/SmithyXML/Writer/Writer+libxml2.swift | 111 +++++ Sources/SmithyXML/Writer/Writer.swift | 202 ++++++++ Sources/SmithyXML/Writer/XMLReadWrite.swift | 17 + Sources/SmithyXML/WritingClosures.swift | 83 ++++ Sources/libxml2/module.modulemap | 5 + Sources/libxml2/shim.h | 7 + .../HttpRequestTestBaseTests.swift | 2 +- .../DateFormatterTests.swift | 2 +- .../Helpers/DateHelpers.swift | 0 .../TimestampSerdeUtilsTests.swift | 2 +- Tests/SmithyXMLTests/XMLEncoderTests.swift | 78 ++++ .../SmithyXMLTests/XMLFloatEncoderTests.swift | 59 +++ .../swift/codegen/ClientRuntimeTypes.kt | 17 +- .../swift/codegen/ShapeValueGenerator.kt | 52 +-- .../swift/codegen/SmithyReadWriteTypes.kt | 25 + .../smithy/swift/codegen/SmithyXMLTypes.kt | 19 + .../swift/codegen/StructureGenerator.kt | 17 +- .../smithy/swift/codegen/SwiftDependency.kt | 16 + .../HttpBindingProtocolGenerator.kt | 17 +- .../HttpProtocolClientGenerator.kt | 3 +- .../integration/HttpProtocolTestGenerator.kt | 20 +- .../HttpProtocolUnitTestGenerator.kt | 5 + .../HttpProtocolUnitTestRequestGenerator.kt | 19 +- .../middlewares/ContentLengthMiddleware.kt | 2 + .../middlewares/ContentMD5Middleware.kt | 3 +- .../middlewares/ContentTypeMiddleware.kt | 2 + .../middlewares/DeserializeMiddleware.kt | 2 + .../middlewares/IdempotencyTokenMiddleware.kt | 3 +- .../middlewares/LoggingMiddleware.kt | 2 + .../OperationInputBodyMiddleware.kt | 189 ++++++-- .../OperationInputHeadersMiddleware.kt | 2 + .../OperationInputQueryItemMiddleware.kt | 2 + .../OperationInputUrlHostMiddleware.kt | 2 + .../OperationInputUrlPathMiddleware.kt | 2 + .../RequestTestEndpointResolverMiddleware.kt | 3 +- .../middlewares/RetryMiddleware.kt | 3 +- .../handlers/HttpBodyMiddleware.kt | 200 -------- .../DynamicNodeDecodingGeneratorStrategy.kt | 14 + .../DynamicNodeEncodingGeneratorStrategy.kt | 37 -- .../serde/UnionEncodeGeneratorStrategy.kt | 4 +- .../serde/json/MemberShapeDecodeGenerator.kt | 5 - .../serde/json/MemberShapeEncodeGenerator.kt | 8 +- .../readwrite/DocumentWritingClosureUtils.kt | 85 ++++ .../serde/readwrite/WritingClosureUtils.kt | 86 ++++ .../xml/DynamicNodeEncodingXMLGenerator.kt | 75 --- .../xml/MemberShapeDecodeXMLGenerator.kt | 8 - .../xml/MemberShapeEncodeXMLGenerator.kt | 431 ++++-------------- .../integration/serde/xml/NodeInfoUtils.kt | 65 +++ .../serde/xml/StructEncodeXMLGenerator.kt | 90 +--- .../serde/xml/UnionDecodeXMLGenerator.kt | 5 - .../serde/xml/UnionEncodeXMLGenerator.kt | 46 +- .../serde/xml/trait/XMLNameTraitGenerator.kt | 3 +- .../xml/trait/XMLNamespaceTraitGenerator.kt | 35 -- .../MiddlewareExecutionGenerator.kt | 17 +- .../middleware/MiddlewareRenderable.kt | 3 +- .../codegen/middleware/OperationMiddleware.kt | 2 + .../OperationMiddlewareGenerator.kt | 6 +- .../smithy/swift/codegen/model/SymbolExt.kt | 13 - .../test/kotlin/ContentMd5MiddlewareTests.kt | 13 +- .../test/kotlin/HttpBodyMiddlewareTests.kt | 177 ------- .../HttpProtocolClientGeneratorTests.kt | 170 ++++--- ...tpProtocolUnitTestRequestGeneratorTests.kt | 57 ++- ...pProtocolUnitTestResponseGeneratorTests.kt | 17 +- .../test/kotlin/IdempotencyTokenTraitTests.kt | 13 +- .../test/kotlin/RecursiveShapeBoxerTests.kt | 4 +- .../test/kotlin/ShapeValueGeneratorTest.kt | 6 +- .../kotlin/StructDecodeGenerationTests.kt | 4 +- .../kotlin/StructEncodeGenerationTests.kt | 4 +- .../test/kotlin/StructureGeneratorTests.kt | 4 +- .../mocks/MockHttpRestXMLProtocolGenerator.kt | 5 +- .../xml/AttributeEncodeXMLGenerationTests.kt | 35 +- .../serde/xml/BlobEncodeXMLGenerationTests.kt | 22 +- .../serde/xml/EnumEncodeXMLGenerationTests.kt | 37 +- .../serde/xml/ListEncodeXMLGenerationTests.kt | 294 ++---------- .../serde/xml/MapEncodeXMLGenerationTests.kt | 414 +++-------------- .../xml/NamespaceEncodeXMLGenerationTests.kt | 286 ++---------- ...RecursiveShapesEncodeXMLGenerationTests.kt | 111 ++--- .../serde/xml/SetEncodeXMLGenerationTests.kt | 25 +- .../xml/StructEncodeXMLGenerationTests.kt | 56 +-- .../xml/TimeStampEncodeGenerationTests.kt | 74 +-- .../xml/UnionEncodeXMLGenerationTests.kt | 42 +- 109 files changed, 2220 insertions(+), 2527 deletions(-) create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobStreamBodyMiddleware.swift rename Sources/ClientRuntime/Networking/Http/Middlewares/{SerializableBodyMiddleware.swift => RequestBody/BodyMiddleware.swift} (51%) create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EnumBodyMiddleware.swift create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EventStreamBodyMiddleware.swift create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/PayloadBodyMiddleware.swift create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/StringBodyMiddleware.swift delete mode 100644 Sources/ClientRuntime/PrimitiveTypeExtensions/BoxType.swift create mode 100644 Sources/ClientRuntime/PrimitiveTypeExtensions/Indirect.swift delete mode 100644 Sources/ClientRuntime/Serialization/Encoder/DynamicNodeEncoding.swift delete mode 100644 Sources/ClientRuntime/Serialization/Encoder/XMLEncoder+Extensions.swift create mode 100644 Sources/ClientRuntime/Serialization/FormURL/Encoder/FormURLReadWrite.swift create mode 100644 Sources/ClientRuntime/Serialization/SerializationUtils/JSONReadWrite.swift create mode 100644 Sources/ClientRuntime/Serialization/SerializationUtils/TimestampFormatter.swift create mode 100644 Sources/ClientRuntime/Serialization/SerializationUtils/XML/ValueWriters.swift create mode 100644 Sources/SmithyReadWrite/DocumentWritingClosure.swift create mode 100644 Sources/SmithyReadWrite/WritingClosure.swift rename Sources/{ClientRuntime/Serialization/SerializationUtils => SmithyTimestamps}/DateFormatters.swift (99%) rename Sources/{ClientRuntime/Serialization/SerializationUtils => SmithyTimestamps}/TimestampSerdeUtils.swift (98%) create mode 100644 Sources/SmithyXML/DocumentWriter.swift create mode 100644 Sources/SmithyXML/Writer/NodeInfo.swift create mode 100644 Sources/SmithyXML/Writer/Writer+libxml2.swift create mode 100644 Sources/SmithyXML/Writer/Writer.swift create mode 100644 Sources/SmithyXML/Writer/XMLReadWrite.swift create mode 100644 Sources/SmithyXML/WritingClosures.swift create mode 100644 Sources/libxml2/module.modulemap create mode 100644 Sources/libxml2/shim.h rename Tests/{ClientRuntimeTests/SerializationTests/SerializationUtilsTests => SmithyTimestampsTests}/DateFormatterTests.swift (99%) rename Tests/{ClientRuntimeTests => SmithyTimestampsTests}/Helpers/DateHelpers.swift (100%) rename Tests/{ClientRuntimeTests/SerializationTests/SerializationUtilsTests => SmithyTimestampsTests}/TimestampSerdeUtilsTests.swift (99%) create mode 100644 Tests/SmithyXMLTests/XMLEncoderTests.swift create mode 100644 Tests/SmithyXMLTests/XMLFloatEncoderTests.swift create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyReadWriteTypes.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyXMLTypes.kt delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/handlers/HttpBodyMiddleware.kt delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/DynamicNodeEncodingGeneratorStrategy.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/readwrite/DocumentWritingClosureUtils.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/readwrite/WritingClosureUtils.kt delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/DynamicNodeEncodingXMLGenerator.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/NodeInfoUtils.kt delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/trait/XMLNamespaceTraitGenerator.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/HttpBodyMiddlewareTests.kt diff --git a/.swiftlint.yml b/.swiftlint.yml index cb1c3c3ce..a4dc80f56 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,6 +3,8 @@ excluded: - Sources/SmithyTestUtil/* - Tests/ClientRuntimeTests/* - Tests/SmithyTestUtilTests/* + - Tests/SmithyXMLTests/* + - Tests/SmithyTimestampsTests/* analyzer_rules: - unused_import diff --git a/Package.swift b/Package.swift index 36e9d0bc3..a175ca0ed 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,9 @@ let package = Package( ], products: [ .library(name: "ClientRuntime", targets: ["ClientRuntime"]), - .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]) + .library(name: "SmithyReadWrite", targets: ["SmithyReadWrite"]), + .library(name: "SmithyXML", targets: ["SmithyXML"]), + .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]), ], dependencies: [ .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.17.0"), @@ -21,22 +23,51 @@ let package = Package( .target( name: "ClientRuntime", dependencies: [ + "SmithyXML", .product(name: "AwsCommonRuntimeKit", package: "aws-crt-swift"), .product(name: "Logging", package: "swift-log"), .product(name: "XMLCoder", package: "XMLCoder") ] ), - .testTarget( - name: "ClientRuntimeTests", - dependencies: ["ClientRuntime", "SmithyTestUtil"] + .target(name: "SmithyReadWrite"), + .target( + name: "SmithyXML", + dependencies: [ + "SmithyReadWrite", + "SmithyTimestamps", + .target(name: "libxml2", condition: .when(platforms: [.linux])) + ] + ), + .systemLibrary( + name: "libxml2", + pkgConfig: "libxml-2.0", + providers: [ + .apt(["libxml2 libxml2-dev"]), + .yum(["libxml2 libxml2-devel"]) + ] + ), + .target( + name: "SmithyTimestamps" ), .target( name: "SmithyTestUtil", dependencies: ["ClientRuntime"] ), + .testTarget( + name: "ClientRuntimeTests", + dependencies: ["ClientRuntime", "SmithyTestUtil"] + ), + .testTarget( + name: "SmithyXMLTests", + dependencies: ["SmithyXML"] + ), + .testTarget( + name: "SmithyTimestampsTests", + dependencies: ["SmithyTimestamps"] + ), .testTarget( name: "SmithyTestUtilTests", dependencies: ["SmithyTestUtil"] - ) + ), ] ) diff --git a/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift b/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift index 61da564d8..f2a13f8c4 100644 --- a/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift +++ b/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift @@ -12,7 +12,7 @@ extension EventStream { public class DefaultMessageEncoderStream: MessageEncoderStream, Stream { let stream: AsyncThrowingStream let messageEncoder: MessageEncoder - let messageSinger: MessageSigner + let messageSigner: MessageSigner let requestEncoder: RequestEncoder var readAsyncIterator: AsyncIterator? @@ -20,11 +20,11 @@ extension EventStream { stream: AsyncThrowingStream, messageEncoder: MessageEncoder, requestEncoder: RequestEncoder, - messageSinger: MessageSigner + messageSigner: MessageSigner ) { self.stream = stream self.messageEncoder = messageEncoder - self.messageSinger = messageSinger + self.messageSigner = messageSigner self.requestEncoder = requestEncoder self.readAsyncIterator = makeAsyncIterator() } @@ -32,7 +32,7 @@ extension EventStream { public struct AsyncIterator: AsyncIteratorProtocol { let stream: AsyncThrowingStream let messageEncoder: MessageEncoder - var messageSinger: MessageSigner + var messageSigner: MessageSigner let requestEncoder: RequestEncoder private var lastMessageSent: Bool = false @@ -42,12 +42,12 @@ extension EventStream { stream: AsyncThrowingStream, messageEncoder: MessageEncoder, requestEncoder: RequestEncoder, - messageSinger: MessageSigner + messageSigner: MessageSigner ) { self.stream = stream self.streamIterator = stream.makeAsyncIterator() self.messageEncoder = messageEncoder - self.messageSinger = messageSinger + self.messageSigner = messageSigner self.requestEncoder = requestEncoder } @@ -56,7 +56,7 @@ extension EventStream { // There are no more messages in the base stream // if we have not sent the last message, send it now guard lastMessageSent else { - let emptySignedMessage = try await messageSinger.signEmpty() + let emptySignedMessage = try await messageSigner.signEmpty() let data = try messageEncoder.encode(message: emptySignedMessage) lastMessageSent = true return data @@ -70,7 +70,7 @@ extension EventStream { let message = try event.marshall(encoder: requestEncoder) // sign the message - let signedMessage = try await messageSinger.sign(message: message) + let signedMessage = try await messageSigner.sign(message: message) // encode again the signed message let data = try messageEncoder.encode(message: signedMessage) @@ -83,7 +83,7 @@ extension EventStream { stream: stream, messageEncoder: messageEncoder, requestEncoder: requestEncoder, - messageSinger: messageSinger + messageSigner: messageSigner ) } diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift new file mode 100644 index 000000000..3d0606e7a --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data + +public struct BlobBodyMiddleware: Middleware { + public let id: Swift.String = "BlobBodyMiddleware" + + let keyPath: KeyPath + + public init(keyPath: KeyPath) { + self.keyPath = keyPath + } + + public func handle(context: Context, + input: SerializeStepInput, + next: H) async throws -> OperationOutput + where H: Handler, + Self.MInput == H.Input, + Self.MOutput == H.Output, + Self.Context == H.Context { + let body = HttpBody.data(input.operationInput[keyPath: keyPath]) + input.builder.withBody(body) + return try await next.handle(context: context, input: input) + } + + public typealias MInput = SerializeStepInput + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobStreamBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobStreamBodyMiddleware.swift new file mode 100644 index 000000000..16b94a7b0 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobStreamBodyMiddleware.swift @@ -0,0 +1,37 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data + +public struct BlobStreamBodyMiddleware: Middleware { + public let id: Swift.String = "BlobStreamBodyMiddleware" + + let keyPath: KeyPath + + public init(keyPath: KeyPath) { + self.keyPath = keyPath + } + + public func handle(context: Context, + input: SerializeStepInput, + next: H) async throws -> OperationOutput + where H: Handler, + Self.MInput == H.Input, + Self.MOutput == H.Output, + Self.Context == H.Context { + if let byteStream = input.operationInput[keyPath: keyPath] { + let body = HttpBody(byteStream: byteStream) + input.builder.withBody(body) + } + return try await next.handle(context: context, input: input) + } + + public typealias MInput = SerializeStepInput + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/SerializableBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BodyMiddleware.swift similarity index 51% rename from Sources/ClientRuntime/Networking/Http/Middlewares/SerializableBodyMiddleware.swift rename to Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BodyMiddleware.swift index 9e06f6e97..208cb29af 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/SerializableBodyMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BodyMiddleware.swift @@ -5,14 +5,24 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct SerializableBodyMiddleware: Middleware { - public let id: Swift.String = "\(String(describing: OperationStackInput.self))BodyMiddleware" +import struct Foundation.Data +import typealias SmithyReadWrite.DocumentWritingClosure +import typealias SmithyReadWrite.WritingClosure - let xmlName: String? +public struct BodyMiddleware: Middleware { + public let id: Swift.String = "BodyMiddleware" - public init(xmlName: String? = nil) { - self.xmlName = xmlName + let documentWritingClosure: DocumentWritingClosure + let inputWritingClosure: WritingClosure + + public init( + documentWritingClosure: @escaping DocumentWritingClosure, + inputWritingClosure: @escaping WritingClosure + ) { + self.documentWritingClosure = documentWritingClosure + self.inputWritingClosure = inputWritingClosure } public func handle(context: Context, @@ -23,13 +33,7 @@ public struct SerializableBodyMiddleware: Middleware where Primitive.RawValue == String { + public let id: Swift.String = "EnumBodyMiddleware" + + let keyPath: KeyPath + + public init(keyPath: KeyPath) { + self.keyPath = keyPath + } + + public func handle(context: Context, + input: SerializeStepInput, + next: H) async throws -> OperationOutput + where H: Handler, + Self.MInput == H.Input, + Self.MOutput == H.Output, + Self.Context == H.Context { + let bodyString = input.operationInput[keyPath: keyPath]?.rawValue ?? "" + let body = HttpBody.data(Data(bodyString.utf8)) + input.builder.withBody(body) + return try await next.handle(context: context, input: input) + } + + public typealias MInput = SerializeStepInput + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EventStreamBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EventStreamBodyMiddleware.swift new file mode 100644 index 000000000..f92ba807f --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EventStreamBodyMiddleware.swift @@ -0,0 +1,60 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import typealias SmithyReadWrite.DocumentWritingClosure +import typealias SmithyReadWrite.WritingClosure + +public struct EventStreamBodyMiddleware: + Middleware { + public let id: Swift.String = "EventStreamBodyMiddleware" + + let keyPath: KeyPath?> + let defaultBody: String? + + public init( + keyPath: KeyPath?>, + defaultBody: String? = nil + ) { + self.keyPath = keyPath + self.defaultBody = defaultBody + } + + public func handle(context: Context, + input: SerializeStepInput, + next: H) async throws -> OperationOutput + where H: Handler, + Self.MInput == H.Input, + Self.MOutput == H.Output, + Self.Context == H.Context { + let encoder = context.getEncoder() + if let eventStream = input.operationInput[keyPath: keyPath] { + guard let messageEncoder = context.getMessageEncoder() else { + fatalError("Message encoder is required for streaming payload") + } + guard let messageSigner = context.getMessageSigner() else { + fatalError("Message signer is required for streaming payload") + } + let encoderStream = EventStream.DefaultMessageEncoderStream( + stream: eventStream, + messageEncoder: messageEncoder, + requestEncoder: encoder, + messageSigner: messageSigner + ) + input.builder.withBody(.stream(encoderStream)) + } else if let defaultBody { + input.builder.withBody(HttpBody.data(Data(defaultBody.utf8))) + } + return try await next.handle(context: context, input: input) + } + + public typealias MInput = SerializeStepInput + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/PayloadBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/PayloadBodyMiddleware.swift new file mode 100644 index 000000000..04e15cbb1 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/PayloadBodyMiddleware.swift @@ -0,0 +1,59 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import typealias SmithyReadWrite.DocumentWritingClosure +import typealias SmithyReadWrite.WritingClosure + +public struct PayloadBodyMiddleware: Middleware { + public let id: Swift.String = "PayloadBodyMiddleware" + + let documentWritingClosure: DocumentWritingClosure + let inputWritingClosure: WritingClosure + let keyPath: KeyPath + let defaultBody: String? + + public init( + documentWritingClosure: @escaping DocumentWritingClosure, + inputWritingClosure: @escaping WritingClosure, + keyPath: KeyPath, + defaultBody: String? + ) { + self.documentWritingClosure = documentWritingClosure + self.inputWritingClosure = inputWritingClosure + self.keyPath = keyPath + self.defaultBody = defaultBody + } + + public func handle(context: Context, + input: SerializeStepInput, + next: H) async throws -> OperationOutput + where H: Handler, + Self.MInput == H.Input, + Self.MOutput == H.Output, + Self.Context == H.Context { + do { + if let payload = input.operationInput[keyPath: keyPath] { + let data = try documentWritingClosure(payload, inputWritingClosure) + let body = HttpBody.data(data) + input.builder.withBody(body) + } else if let defaultBody { + input.builder.withBody(HttpBody.data(Data(defaultBody.utf8))) + } + } catch { + throw ClientError.serializationFailed(error.localizedDescription) + } + return try await next.handle(context: context, input: input) + } + + public typealias MInput = SerializeStepInput + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/StringBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/StringBodyMiddleware.swift new file mode 100644 index 000000000..cacbfe39c --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/StringBodyMiddleware.swift @@ -0,0 +1,35 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data + +public struct StringBodyMiddleware: Middleware { + public let id: Swift.String = "\(OperationStackInput.self)StringBodyMiddleware" + + let keyPath: KeyPath + + public init(keyPath: KeyPath) { + self.keyPath = keyPath + } + + public func handle(context: Context, + input: SerializeStepInput, + next: H) async throws -> OperationOutput + where H: Handler, + Self.MInput == H.Input, + Self.MOutput == H.Output, + Self.Context == H.Context { + let body = HttpBody.data(Data((input.operationInput[keyPath: keyPath] ?? "").utf8)) + input.builder.withBody(body) + return try await next.handle(context: context, input: input) + } + + public typealias MInput = SerializeStepInput + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} diff --git a/Sources/ClientRuntime/PrimitiveTypeExtensions/BoxType.swift b/Sources/ClientRuntime/PrimitiveTypeExtensions/BoxType.swift deleted file mode 100644 index 1900cc102..000000000 --- a/Sources/ClientRuntime/PrimitiveTypeExtensions/BoxType.swift +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -public final class Box { - public var value: T - - public init(value: T) { - self.value = value - } -} - -extension Box: Equatable where T: Equatable { - public static func == (lhs: Box, rhs: Box) -> Bool { - return lhs.value == rhs.value - } -} - -extension Box: Codable where T: Codable { - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(value) - } - - public convenience init(from decoder: Decoder) throws { - let values = try decoder.singleValueContainer() - let value = try values.decode(T.self) - self.init(value: value) - } -} diff --git a/Sources/ClientRuntime/PrimitiveTypeExtensions/Indirect.swift b/Sources/ClientRuntime/PrimitiveTypeExtensions/Indirect.swift new file mode 100644 index 000000000..ac98b34d8 --- /dev/null +++ b/Sources/ClientRuntime/PrimitiveTypeExtensions/Indirect.swift @@ -0,0 +1,22 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +@propertyWrapper +public class Indirect { + public var wrappedValue: T? + + public init(wrappedValue: T? = nil) { + self.wrappedValue = wrappedValue + } +} + +extension Indirect: Equatable where T: Equatable { + + public static func ==(lhs: Indirect, rhs: Indirect) -> Bool { + lhs.wrappedValue == rhs.wrappedValue + } +} diff --git a/Sources/ClientRuntime/Serialization/Encoder/DynamicNodeEncoding.swift b/Sources/ClientRuntime/Serialization/Encoder/DynamicNodeEncoding.swift deleted file mode 100644 index 8195a1c83..000000000 --- a/Sources/ClientRuntime/Serialization/Encoder/DynamicNodeEncoding.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import XMLCoder - -public typealias DynamicNodeEncoding = XMLCoder.DynamicNodeEncoding -public typealias NodeEncoding = XMLEncoder.NodeEncoding diff --git a/Sources/ClientRuntime/Serialization/Encoder/RequestEncoder.swift b/Sources/ClientRuntime/Serialization/Encoder/RequestEncoder.swift index 485cc7051..5dadf5f49 100644 --- a/Sources/ClientRuntime/Serialization/Encoder/RequestEncoder.swift +++ b/Sources/ClientRuntime/Serialization/Encoder/RequestEncoder.swift @@ -3,10 +3,6 @@ * SPDX-License-Identifier: Apache-2.0. */ -import Foundation - public protocol RequestEncoder { - - func encode(_ value: T) throws -> Data where T: Encodable - + func encode(_ value: T) throws -> Data } diff --git a/Sources/ClientRuntime/Serialization/Encoder/XMLEncoder+Extensions.swift b/Sources/ClientRuntime/Serialization/Encoder/XMLEncoder+Extensions.swift deleted file mode 100644 index 1f566f4f7..000000000 --- a/Sources/ClientRuntime/Serialization/Encoder/XMLEncoder+Extensions.swift +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import Foundation -import XMLCoder - -public typealias XMLEncoder = XMLCoder.XMLEncoder -extension XMLEncoder: RequestEncoder { - public func encode(_ value: T) throws -> Data where T: Encodable { - return try encode(value, withRootKey: nil, rootAttributes: nil, header: nil) - } -} diff --git a/Sources/ClientRuntime/Serialization/FormURL/Encoder/FormURLReadWrite.swift b/Sources/ClientRuntime/Serialization/FormURL/Encoder/FormURLReadWrite.swift new file mode 100644 index 000000000..107fc7912 --- /dev/null +++ b/Sources/ClientRuntime/Serialization/FormURL/Encoder/FormURLReadWrite.swift @@ -0,0 +1,41 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import typealias SmithyReadWrite.DocumentWritingClosure +import typealias SmithyReadWrite.WritingClosure + +public class FormURLWriter { + private let encoder: any RequestEncoder + var data = Data() + + init(encoder: any RequestEncoder) { + self.encoder = encoder + } + + func encode(_ value: T) throws { + self.data = try encoder.encode(value) + } +} + +public enum FormURLReadWrite { + + public static func documentWritingClosure( + encoder: RequestEncoder + ) -> DocumentWritingClosure { + return { value, writingClosure in + let formURLWriter = FormURLWriter(encoder: encoder) + try writingClosure(value, formURLWriter) + return formURLWriter.data + } + } + + public static func writingClosure() -> WritingClosure { + return { value, writer in + try writer.encode(value) + } + } +} diff --git a/Sources/ClientRuntime/Serialization/SerializationUtils/JSONReadWrite.swift b/Sources/ClientRuntime/Serialization/SerializationUtils/JSONReadWrite.swift new file mode 100644 index 000000000..6a3cf76b1 --- /dev/null +++ b/Sources/ClientRuntime/Serialization/SerializationUtils/JSONReadWrite.swift @@ -0,0 +1,40 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SmithyReadWrite + +public class JSONWriter { + private let encoder: any RequestEncoder + var data = Data() + + init(encoder: any RequestEncoder) { + self.encoder = encoder + } + + func encode(_ value: T) throws { + self.data = try encoder.encode(value) + } +} + +public enum JSONReadWrite { + + public static func documentWritingClosure( + encoder: RequestEncoder + ) -> DocumentWritingClosure { + return { value, writingClosure in + let jsonEncoder = JSONWriter(encoder: encoder) + try writingClosure(value, jsonEncoder) + return jsonEncoder.data + } + } + + public static func writingClosure() -> WritingClosure { + return { value, writer in + try writer.encode(value) + } + } +} diff --git a/Sources/ClientRuntime/Serialization/SerializationUtils/TimestampFormatter.swift b/Sources/ClientRuntime/Serialization/SerializationUtils/TimestampFormatter.swift new file mode 100644 index 000000000..c2b3eb587 --- /dev/null +++ b/Sources/ClientRuntime/Serialization/SerializationUtils/TimestampFormatter.swift @@ -0,0 +1,11 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import SmithyTimestamps + +public typealias TimestampFormatter = SmithyTimestamps.TimestampFormatter +public typealias TimestampFormat = SmithyTimestamps.TimestampFormat diff --git a/Sources/ClientRuntime/Serialization/SerializationUtils/XML/MapEntry.swift b/Sources/ClientRuntime/Serialization/SerializationUtils/XML/MapEntry.swift index c192c5788..855c93c99 100644 --- a/Sources/ClientRuntime/Serialization/SerializationUtils/XML/MapEntry.swift +++ b/Sources/ClientRuntime/Serialization/SerializationUtils/XML/MapEntry.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct MapEntry: Codable where K: Codable, V: Codable { +public struct MapEntry: Decodable where K: Decodable, V: Decodable { public let entry: [MapKeyValue]? public enum CodingKeys: String, CodingKey { case entry diff --git a/Sources/ClientRuntime/Serialization/SerializationUtils/XML/MapKeyValue.swift b/Sources/ClientRuntime/Serialization/SerializationUtils/XML/MapKeyValue.swift index 4ed53572a..0b8cf1953 100644 --- a/Sources/ClientRuntime/Serialization/SerializationUtils/XML/MapKeyValue.swift +++ b/Sources/ClientRuntime/Serialization/SerializationUtils/XML/MapKeyValue.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct MapKeyValue: Codable where K: Codable, V: Codable { +public struct MapKeyValue: Decodable where K: Decodable, V: Decodable { public let key: K public let value: V diff --git a/Sources/ClientRuntime/Serialization/SerializationUtils/XML/ValueWriters.swift b/Sources/ClientRuntime/Serialization/SerializationUtils/XML/ValueWriters.swift new file mode 100644 index 000000000..42e599a0e --- /dev/null +++ b/Sources/ClientRuntime/Serialization/SerializationUtils/XML/ValueWriters.swift @@ -0,0 +1,22 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import class SmithyXML.Writer + +extension Writer { + + public func write(_ value: ByteStream?) throws { + // This serialization will never be performed in practice, since + // a ByteStream will never be a part of + // a XML body - if there is a streaming member in a restXml + // input shape, the rest of the input members must all be bound + // to HTML components outside the body. + // + // This empty implementation is only provided to quiet the + // compiler when a structure with a ByteSteam is code-generated. + } +} diff --git a/Sources/SmithyReadWrite/DocumentWritingClosure.swift b/Sources/SmithyReadWrite/DocumentWritingClosure.swift new file mode 100644 index 000000000..d5645afc1 --- /dev/null +++ b/Sources/SmithyReadWrite/DocumentWritingClosure.swift @@ -0,0 +1,10 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data + +public typealias DocumentWritingClosure = (T, WritingClosure) throws -> Data diff --git a/Sources/SmithyReadWrite/WritingClosure.swift b/Sources/SmithyReadWrite/WritingClosure.swift new file mode 100644 index 000000000..d0e36b5b7 --- /dev/null +++ b/Sources/SmithyReadWrite/WritingClosure.swift @@ -0,0 +1,9 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public typealias WritingClosure = + (T, Writer) throws -> Void diff --git a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift index 9fb67aade..c7e250213 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift @@ -221,9 +221,10 @@ open class HttpRequestTestBase: XCTestCase { } public func genericAssertEqualHttpBodyData( - _ expected: HttpBody, - _ actual: HttpBody, - _ encoder: Any, + expected: HttpBody, + actual: HttpBody, + isXML: Bool, + isJSON: Bool, _ callback: (Data, Data) -> Void, file: StaticString = #filePath, line: UInt = #line @@ -231,9 +232,9 @@ open class HttpRequestTestBase: XCTestCase { let expectedData = try await expected.readData() let actualData = try await actual.readData() if shouldCompareData(expectedData, actualData) { - if encoder is XMLEncoder { + if isXML { XCTAssertXMLDataEqual(actualData!, expectedData!, file: file, line: line) - } else if encoder is JSONEncoder { + } else if isJSON { XCTAssertJSONDataEqual(actualData!, expectedData!, file: file, line: line) } callback(expectedData!, actualData!) diff --git a/Sources/ClientRuntime/Serialization/SerializationUtils/DateFormatters.swift b/Sources/SmithyTimestamps/DateFormatters.swift similarity index 99% rename from Sources/ClientRuntime/Serialization/SerializationUtils/DateFormatters.swift rename to Sources/SmithyTimestamps/DateFormatters.swift index 045f27135..9c9db396c 100644 --- a/Sources/ClientRuntime/Serialization/SerializationUtils/DateFormatters.swift +++ b/Sources/SmithyTimestamps/DateFormatters.swift @@ -6,6 +6,7 @@ import class Foundation.DateFormatter import struct Foundation.TimeZone import struct Foundation.Locale +import struct Foundation.Date public typealias DateFormatter = Foundation.DateFormatter diff --git a/Sources/ClientRuntime/Serialization/SerializationUtils/TimestampSerdeUtils.swift b/Sources/SmithyTimestamps/TimestampSerdeUtils.swift similarity index 98% rename from Sources/ClientRuntime/Serialization/SerializationUtils/TimestampSerdeUtils.swift rename to Sources/SmithyTimestamps/TimestampSerdeUtils.swift index 98d8e7675..aefa6c0b8 100644 --- a/Sources/ClientRuntime/Serialization/SerializationUtils/TimestampSerdeUtils.swift +++ b/Sources/SmithyTimestamps/TimestampSerdeUtils.swift @@ -6,6 +6,7 @@ // import func Foundation.floor +import struct Foundation.Date /// Custom timestamp serialization formats /// https://smithy.io/2.0/spec/protocol-traits.html#smithy-api-timestampformat-trait @@ -251,11 +252,11 @@ extension DecodingError { } } -extension ClientRuntime.Date { +extension Date { /// Creates a date from a string using the given formatters. /// The date returned will be from the first formatter, in the given formatters list, that is able to successfully convert the date to a string. /// Returns `nil` if the none of the given formatters were able to create a date from the given string or if formatters is empty. - init?(from string: String, formatters: [ClientRuntime.DateFormatter]) { + init?(from string: String, formatters: [DateFormatter]) { for formatter in formatters { if let date = formatter.date(from: string) { self = date diff --git a/Sources/SmithyXML/DocumentWriter.swift b/Sources/SmithyXML/DocumentWriter.swift new file mode 100644 index 000000000..c8c847b02 --- /dev/null +++ b/Sources/SmithyXML/DocumentWriter.swift @@ -0,0 +1,18 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Data +import typealias SmithyReadWrite.WritingClosure + +public enum DocumentWriter { + + static func write(_ value: T, rootNodeInfo: NodeInfo, writingClosure: WritingClosure) throws -> Data { + let writer = Writer(rootNodeInfo: rootNodeInfo) + try writingClosure(value, writer) + return writer.xmlString() + } +} diff --git a/Sources/SmithyXML/Writer/NodeInfo.swift b/Sources/SmithyXML/Writer/NodeInfo.swift new file mode 100644 index 000000000..942e5ebc4 --- /dev/null +++ b/Sources/SmithyXML/Writer/NodeInfo.swift @@ -0,0 +1,34 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct NodeInfo { + + public enum Location { + case element + case attribute + } + + public struct Namespace: Equatable { + let prefix: String + let uri: String + + public init(prefix: String, uri: String) { + self.prefix = prefix + self.uri = uri + } + } + + public let name: String + public let location: Location + public let namespace: Namespace? + + public init(_ name: String, location: Location = .element, namespace: Namespace? = nil) { + self.name = name + self.location = location + self.namespace = namespace + } +} diff --git a/Sources/SmithyXML/Writer/Writer+libxml2.swift b/Sources/SmithyXML/Writer/Writer+libxml2.swift new file mode 100644 index 000000000..8a71a085f --- /dev/null +++ b/Sources/SmithyXML/Writer/Writer+libxml2.swift @@ -0,0 +1,111 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import libxml2 +import struct Foundation.Data + +/// Extends Writer to copy its tree into libxml2, then write the tree to XML data. +extension Writer { + + /// Translates this Writer and its children into XML ready to be sent. + /// - Returns: A `Data` value containing this writer's UTF-8 XML representation. + func xmlString() -> Data { + // Create a libxml document + let doc = xmlNewDoc(nil) + + // Create the tree and set the root node on the document + let rootNode = nodify(to: nil, doc: doc) + xmlDocSetRootElement(doc, rootNode) + + // Create a buffer to hold the XML data + let buffer = xmlBufferCreate() + + // Write the XML to the buffer + xmlNodeDump(buffer, doc, rootNode, 0, 0) + + // Transfer the buffer to a Swift Data value + var data = Data() + if let buffer { + data = Data(bytes: buffer.pointee.content, count: Int(buffer.pointee.use)) + } + + // Free up memory and return data + xmlFreeDoc(doc) + xmlFree(buffer) + return data + } + + /// Translates the data in this `Writer` to a libxml2 node. + /// + /// Used to transform the `Writer` tree into a corresponding tree of libxml nodes for rendering to XML. + /// - Parameters: + /// - parentNode: The libxml2 parent node to attach this node to as a child, if any. + /// - doc: The libxml2 document these nodes are a part of. + /// - Returns: The libxml2 node that represents this `Writer`, with libxml2 children nodes for all the `Writer`'s children. + private func nodify(to parentNode: xmlNodePtr?, doc: xmlDocPtr?) -> xmlNodePtr? { + + // Expose the node name and content as C strings + nodeInfo.name.utf8CString.withUnsafeBytes { unsafeName in + content.utf8CString.withUnsafeBytes { unsafeContent in + + // libxml uses C strings with its own xmlChar data type + // Recast the C strings to libxml-typed strings + let name = UnsafePointer(unsafeName.bindMemory(to: xmlChar.self).baseAddress) + let content = UnsafePointer(unsafeContent.bindMemory(to: xmlChar.self).baseAddress) + + // Create a node and set its name and type + let node = xmlNewNode(nil, name) + node?.pointee.type = nodeInfo.location.xmlElementType + + // Encode the content string, set it on the node, then free it + let encoded = xmlEncodeEntitiesReentrant(doc, content) + xmlNodeSetContent(node, encoded) + xmlFree(encoded) + + // Add the child node to its parent + xmlAddChild(parentNode, node) + + // Unwrap the namespace if any, then access its prefix & uri as C strings + if let namespace = nodeInfo.namespace { + namespace.prefix.utf8CString.withUnsafeBytes { unsafePrefix in + namespace.uri.utf8CString.withUnsafeBytes { unsafeURI in + + // libxml uses C strings with its own xmlChar data type + // Recast the C strings to libxml-typed strings + let prefix = UnsafePointer(unsafePrefix.bindMemory(to: xmlChar.self).baseAddress) + let uri = UnsafePointer(unsafeURI.bindMemory(to: xmlChar.self).baseAddress) + + // Add the namespace to the node + // If the prefix is an empty string, replace it with nil and libxml will + // fill in default prefix ("xmlns") for you + xmlNewNs(node, uri, prefix?.pointee == 0 ? nil : prefix) + } + } + } + + // Nodify all of this writer's children and add them to the node as children + for child in self.children { + _ = child.nodify(to: node, doc: doc) + } + + // Return the node. Only the root node return value is used + return node + } + } + } +} + +private extension NodeInfo.Location { + + /// Translates NodeInfo's `Location` property into the corresponding libxml element type. + var xmlElementType: xmlElementType { + switch self { + case .element: return XML_ELEMENT_NODE + case .attribute: return XML_ATTRIBUTE_NODE + } + } +} diff --git a/Sources/SmithyXML/Writer/Writer.swift b/Sources/SmithyXML/Writer/Writer.swift new file mode 100644 index 000000000..7b56f44fd --- /dev/null +++ b/Sources/SmithyXML/Writer/Writer.swift @@ -0,0 +1,202 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Date +import struct Foundation.Data +import typealias SmithyReadWrite.WritingClosure +import enum SmithyTimestamps.TimestampFormat +import struct SmithyTimestamps.TimestampFormatter + +/// A class used to encode a tree of model data as XML. +/// +/// Custom types (i.e. structures and unions) that are to be written as XML need to provide +/// a writing closure. A writing closure is code generated for Smithy model types. +/// +/// This writer will write all Swift types used by Smithy models, and will also write Swift +/// `Array` and `Dictionary` (optionally as flattened XML) given a writing closure for +/// their enclosed data types. +public class Writer { + var content = "" + var children: [Writer] = [] + weak var parent: Writer? + let nodeInfo: NodeInfo + let nodeInfoPath: [NodeInfo] + + // MARK: - init & deinit + + /// Used by the `DocumentWriter` to begin serialization of a model to XML. + /// - Parameter rootNodeInfo: The node info for the root XML node. + init(rootNodeInfo: NodeInfo) { + self.nodeInfo = rootNodeInfo + self.nodeInfoPath = [rootNodeInfo] + } + + private init(nodeInfo: NodeInfo, nodeInfoPath: [NodeInfo], parent: Writer?) { + self.nodeInfo = nodeInfo + self.nodeInfoPath = nodeInfoPath + self.parent = parent + } + + // MARK: - creating and detaching writers for subelements + + public subscript(_ nodeInfo: NodeInfo) -> Writer { + let namespace = nodeInfoPath.compactMap { $0.namespace }.contains(nodeInfo.namespace) ? nil : nodeInfo.namespace + let newNodeInfo = NodeInfo(nodeInfo.name, location: nodeInfo.location, namespace: namespace) + let newChild = Writer(nodeInfo: newNodeInfo, nodeInfoPath: nodeInfoPath + [newNodeInfo], parent: self) + addChild(newChild) + return newChild + } + + /// Detaches this writer from its parent. Typically used when this writer no longer + /// belongs in the tree, either because its data is nil or its contents were flattened + /// into its parents. + public func detach() { + parent?.children.removeAll { $0 === self } + parent = nil + } + + // MARK: - Writing values + + public func write(_ value: Bool?) throws { + record(string: value.map { $0 ? "true" : "false" }) + } + + public func write(_ value: String?) throws { + record(string: value) + } + + public func write(_ value: Double?) throws { + guard let value else { detach(); return } + guard !value.isNaN else { + record(string: "NaN") + return + } + switch value { + case .infinity: + record(string: "Infinity") + case -.infinity: + record(string: "-Infinity") + default: + record(string: "\(value)") + } + } + + public func write(_ value: Float?) throws { + guard let value else { detach(); return } + guard !value.isNaN else { + record(string: "NaN") + return + } + switch value { + case .infinity: + record(string: "Infinity") + case -.infinity: + record(string: "-Infinity") + default: + record(string: "\(value)") + } + } + + public func write(_ value: Int?) throws { + record(string: value.map { "\($0)" }) + } + + public func write(_ value: Int8?) throws { + record(string: value.map { "\($0)" }) + } + + public func write(_ value: Int16?) throws { + record(string: value.map { "\($0)" }) + } + + public func write(_ value: UInt8?) throws { + record(string: value.map { "\($0)" }) + } + + public func write(_ value: Data?) throws { + try write(value?.base64EncodedString()) + } + + public func writeTimestamp(_ value: Date?, format: TimestampFormat) throws { + guard let value else { detach(); return } + record(string: TimestampFormatter(format: format).string(from: value)) + } + + public func write(_ value: T?) throws where T.RawValue == Int { + try write(value?.rawValue) + } + + public func write(_ value: T?) throws where T.RawValue == String { + try write(value?.rawValue) + } + + public func writeMap( + _ value: [String: T]?, + valueWritingClosure: WritingClosure, + keyNodeInfo: NodeInfo, + valueNodeInfo: NodeInfo, + isFlattened: Bool + ) throws { + guard let value else { detach(); return } + if isFlattened { + defer { detach() } + guard let parent = self.parent else { return } + for (key, value) in value { + let entryWriter = parent[.init(nodeInfo.name)] + try entryWriter[keyNodeInfo].write(key) + try valueWritingClosure(value, entryWriter[valueNodeInfo]) + } + } else { + for (key, value) in value { + let entryWriter = self[.init("entry")] + try entryWriter[keyNodeInfo].write(key) + try valueWritingClosure(value, entryWriter[valueNodeInfo]) + } + } + } + + public func writeList( + _ value: [T]?, + memberWritingClosure: WritingClosure, + memberNodeInfo: NodeInfo, + isFlattened: Bool + ) throws { + guard let value else { detach(); return } + if isFlattened { + defer { detach() } + guard let parent = self.parent, !nodeInfo.name.isEmpty else { return } + let flattenedMemberNodeInfo = NodeInfo( + nodeInfo.name, + location: memberNodeInfo.location, + namespace: memberNodeInfo.namespace + ) + for member in value { + try memberWritingClosure(member, parent[flattenedMemberNodeInfo]) + } + } else { + for member in value { + try memberWritingClosure(member, self[memberNodeInfo]) + } + } + } + + public func write(_ value: T, writingClosure: WritingClosure) throws { + try writingClosure(value, self) + } + + // MARK: - Private methods + + private func addChild(_ child: Writer) { + children.append(child) + child.parent = self + } + + private func record(string: String?) { + guard let string else { detach(); return } + content = string + } +} diff --git a/Sources/SmithyXML/Writer/XMLReadWrite.swift b/Sources/SmithyXML/Writer/XMLReadWrite.swift new file mode 100644 index 000000000..b124e83b5 --- /dev/null +++ b/Sources/SmithyXML/Writer/XMLReadWrite.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import typealias SmithyReadWrite.DocumentWritingClosure + +public enum XMLReadWrite { + + public static func documentWritingClosure(rootNodeInfo: NodeInfo) -> DocumentWritingClosure { + return { value, writingClosure in + try DocumentWriter.write(value, rootNodeInfo: rootNodeInfo, writingClosure: writingClosure) + } + } +} diff --git a/Sources/SmithyXML/WritingClosures.swift b/Sources/SmithyXML/WritingClosures.swift new file mode 100644 index 000000000..c8a492c37 --- /dev/null +++ b/Sources/SmithyXML/WritingClosures.swift @@ -0,0 +1,83 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.Date +import typealias SmithyReadWrite.WritingClosure +import enum SmithyTimestamps.TimestampFormat + +public func mapWritingClosure( + valueWritingClosure: @escaping WritingClosure, + keyNodeInfo: NodeInfo, + valueNodeInfo: NodeInfo, + isFlattened: Bool +) -> WritingClosure<[String: T], Writer> { + return { map, writer in + try writer.writeMap( + map, + valueWritingClosure: valueWritingClosure, + keyNodeInfo: keyNodeInfo, + valueNodeInfo: valueNodeInfo, + isFlattened: isFlattened + ) + } +} + +public func listWritingClosure( + memberWritingClosure: @escaping WritingClosure, + memberNodeInfo: NodeInfo, + isFlattened: Bool +) -> WritingClosure<[T], Writer> { + return { array, writer in + try writer.writeList( + array, + memberWritingClosure: memberWritingClosure, + memberNodeInfo: memberNodeInfo, + isFlattened: isFlattened + ) + } +} + +public func timestampWritingClosure(format: TimestampFormat) -> WritingClosure { + return { date, writer in + try writer.writeTimestamp(date, format: format) + } +} + +public extension String { + + static func writingClosure(_ value: String?, to writer: Writer) throws { + try writer.write(value) + } +} + +public extension RawRepresentable where RawValue == Int { + + static func writingClosure(_ value: Self?, to writer: Writer) throws { + try writer.write(value?.rawValue) + } +} + +public extension RawRepresentable where RawValue == String { + + static func writingClosure(_ value: Self?, to writer: Writer) throws { + try writer.write(value?.rawValue) + } +} + +public extension Bool { + + static func writingClosure(_ value: Bool?, to writer: Writer) throws { + try writer.write(value) + } +} + +public extension Int { + + static func writingClosure(_ value: Int?, to writer: Writer) throws { + try writer.write(value) + } +} diff --git a/Sources/libxml2/module.modulemap b/Sources/libxml2/module.modulemap new file mode 100644 index 000000000..92eb24fca --- /dev/null +++ b/Sources/libxml2/module.modulemap @@ -0,0 +1,5 @@ +module libxml2 { + header "shim.h" + link "xml2" + export * +} diff --git a/Sources/libxml2/shim.h b/Sources/libxml2/shim.h new file mode 100644 index 000000000..687212c9e --- /dev/null +++ b/Sources/libxml2/shim.h @@ -0,0 +1,7 @@ +#ifndef LibXMLShim_h +#define LibXMLShim_h + +#import +#import +#import +#endif \ No newline at end of file diff --git a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift index c97847720..65816ebee 100644 --- a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift +++ b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift @@ -212,7 +212,7 @@ class HttpRequestTestBaseTests: HttpRequestTestBase { try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) throws -> Void in XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, JSONEncoder()) { (expectedData, actualData) in + try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { (expectedData, actualData) in do { let decoder = JSONDecoder() let expectedObj = try decoder.decode(SayHelloInputBody.self, from: expectedData) diff --git a/Tests/ClientRuntimeTests/SerializationTests/SerializationUtilsTests/DateFormatterTests.swift b/Tests/SmithyTimestampsTests/DateFormatterTests.swift similarity index 99% rename from Tests/ClientRuntimeTests/SerializationTests/SerializationUtilsTests/DateFormatterTests.swift rename to Tests/SmithyTimestampsTests/DateFormatterTests.swift index 042818316..e40f9644c 100644 --- a/Tests/ClientRuntimeTests/SerializationTests/SerializationUtilsTests/DateFormatterTests.swift +++ b/Tests/SmithyTimestampsTests/DateFormatterTests.swift @@ -4,7 +4,7 @@ */ import XCTest -@testable import ClientRuntime +@testable import SmithyTimestamps class DateFormatterTests: XCTestCase { diff --git a/Tests/ClientRuntimeTests/Helpers/DateHelpers.swift b/Tests/SmithyTimestampsTests/Helpers/DateHelpers.swift similarity index 100% rename from Tests/ClientRuntimeTests/Helpers/DateHelpers.swift rename to Tests/SmithyTimestampsTests/Helpers/DateHelpers.swift diff --git a/Tests/ClientRuntimeTests/SerializationTests/SerializationUtilsTests/TimestampSerdeUtilsTests.swift b/Tests/SmithyTimestampsTests/TimestampSerdeUtilsTests.swift similarity index 99% rename from Tests/ClientRuntimeTests/SerializationTests/SerializationUtilsTests/TimestampSerdeUtilsTests.swift rename to Tests/SmithyTimestampsTests/TimestampSerdeUtilsTests.swift index ac6aa4954..3512e0131 100644 --- a/Tests/ClientRuntimeTests/SerializationTests/SerializationUtilsTests/TimestampSerdeUtilsTests.swift +++ b/Tests/SmithyTimestampsTests/TimestampSerdeUtilsTests.swift @@ -6,7 +6,7 @@ // import XCTest -@testable import ClientRuntime +@testable import SmithyTimestamps class TimestampSerdeUtilsTests: XCTestCase { diff --git a/Tests/SmithyXMLTests/XMLEncoderTests.swift b/Tests/SmithyXMLTests/XMLEncoderTests.swift new file mode 100644 index 000000000..eb3e63f70 --- /dev/null +++ b/Tests/SmithyXMLTests/XMLEncoderTests.swift @@ -0,0 +1,78 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@_spi(SmithyXML) import SmithyXML + +class XMLEncoderTests: XCTestCase { + + private struct HasNestedElements: Encodable { + + static func write(_ value: HasNestedElements, to writer: Writer) throws { + try writer[.init("a")].write(value.a) + try writer[.init("b")].write(value.b) + } + + let a: String + let b: String + } + + func test_encodesXMLWithNestedElements() throws { + let data = try SmithyXML.XMLReadWrite.documentWritingClosure( + rootNodeInfo: .init("test") + )( + HasNestedElements(a: "a", b: "b"), + HasNestedElements.write(_:to:) + ) + let xml = "ab" + XCTAssertEqual(String(data: data, encoding: .utf8), xml) + } + + private struct HasNestedElementAndAttribute: Encodable { + + static func write(_ value: HasNestedElementAndAttribute, to writer: Writer) throws { + try writer[.init("a")].write(value.a) + try writer[.init("b", location: .attribute)].write(value.b) + } + + let a: String + let b: String + } + + func test_encodesXMLWithElementAndAttribute() throws { + let data = try SmithyXML.XMLReadWrite.documentWritingClosure( + rootNodeInfo: .init("test") + )( + HasNestedElementAndAttribute(a: "a", b: "b"), + HasNestedElementAndAttribute.write(_:to:) + ) + let xml = "a" + XCTAssertEqual(String(data: data, encoding: .utf8), xml) + } + + func test_encodesXMLWithElementAndAttributeAndNamespace() throws { + let data = try SmithyXML.XMLReadWrite.documentWritingClosure( + rootNodeInfo: .init("test", namespace: .init(prefix: "", uri: "https://www.def.com/1.0")) + )( + HasNestedElementAndAttribute(a: "a", b: "b"), + HasNestedElementAndAttribute.write(_:to:) + ) + let xml = "a" + XCTAssertEqual(String(data: data, encoding: .utf8), xml) + } + + func test_encodesXMLWithElementAndAttributeAndSpecialChars() throws { + let data = try SmithyXML.XMLReadWrite.documentWritingClosure( + rootNodeInfo: .init("test") + )( + HasNestedElementAndAttribute(a: "''", b: "\"b&s\""), + HasNestedElementAndAttribute.write(_:to:) + ) + let xml = "\'<a&z>\'" + XCTAssertEqual(String(data: data, encoding: .utf8), xml) + } +} diff --git a/Tests/SmithyXMLTests/XMLFloatEncoderTests.swift b/Tests/SmithyXMLTests/XMLFloatEncoderTests.swift new file mode 100644 index 000000000..de5459572 --- /dev/null +++ b/Tests/SmithyXMLTests/XMLFloatEncoderTests.swift @@ -0,0 +1,59 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +import SmithyXML + +class XMLFloatEncoderTests: XCTestCase { + + private struct HasFPElements { + + static func write(_ value: HasFPElements, to writer: Writer) throws { + try writer[.init("f")].write(value.f) + try writer[.init("d")].write(value.d) + } + + let f: Float + let d: Double + } + + func test_serializesInfinity() throws { + let fp = HasFPElements(f: .infinity, d: .infinity) + let actualData = try SmithyXML.XMLReadWrite.documentWritingClosure( + rootNodeInfo: .init("fp") + )( + fp, + HasFPElements.write(_:to:) + ) + let expectedData = Data("InfinityInfinity".utf8) + XCTAssertEqual(actualData, expectedData) + } + + func test_serializesNegativeInfinity() throws { + let fp = HasFPElements(f: -.infinity, d: -.infinity) + let actualData = try SmithyXML.XMLReadWrite.documentWritingClosure( + rootNodeInfo: .init("fp") + )( + fp, + HasFPElements.write(_:to:) + ) + let expectedData = Data("-Infinity-Infinity".utf8) + XCTAssertEqual(actualData, expectedData) + } + + func test_serializesNaN() throws { + let fp = HasFPElements(f: .nan, d: .nan) + let actualData = try SmithyXML.XMLReadWrite.documentWritingClosure( + rootNodeInfo: .init("fp") + )( + fp, + HasFPElements.write(_:to:) + ) + let expectedData = Data("NaNNaN".utf8) + XCTAssertEqual(actualData, expectedData) + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt index be7075e70..f4b8e8922 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt @@ -22,7 +22,6 @@ object ClientRuntimeTypes { val SdkHttpClient = runtimeSymbol("SdkHttpClient") val SdkHttpRequestBuilder = runtimeSymbol("SdkHttpRequestBuilder") val SdkHttpRequest = runtimeSymbol("SdkHttpRequest") - val HttpBody = runtimeSymbol("HttpBody") val HttpResponse = runtimeSymbol("HttpResponse") val HttpResponseBinding = runtimeSymbol("HttpResponseBinding") val HttpResponseErrorBinding = runtimeSymbol("HttpResponseErrorBinding") @@ -36,19 +35,20 @@ object ClientRuntimeTypes { val ResponseDecoder = runtimeSymbol("ResponseDecoder") val Key = runtimeSymbol("Key") val DynamicNodeDecoding = runtimeSymbol("DynamicNodeDecoding") - val DynamicNodeEncoding = runtimeSymbol("DynamicNodeEncoding") val NodeDecoding = runtimeSymbol("NodeDecoding") - val NodeEncoding = runtimeSymbol("NodeEncoding") val MapEntry = runtimeSymbol("MapEntry") val CollectionMember = runtimeSymbol("CollectionMember") val MapKeyValue = runtimeSymbol("MapKeyValue") val FormURLEncoder = runtimeSymbol("FormURLEncoder") val JSONDecoder = runtimeSymbol("JSONDecoder") val JSONEncoder = runtimeSymbol("JSONEncoder") - val XMLEncoder = runtimeSymbol("XMLEncoder") + val JSONWriter = runtimeSymbol("JSONWriter") + val FormURLWriter = runtimeSymbol("FormURLWriter") val XMLDecoder = runtimeSymbol("XMLDecoder") val MessageMarshallable = runtimeSymbol("MessageMarshallable") val MessageUnmarshallable = runtimeSymbol("MessageUnmarshallable") + val JSONReadWrite = runtimeSymbol("JSONReadWrite") + val FormURLReadWrite = runtimeSymbol("FormURLReadWrite") } object EventStream { @@ -73,10 +73,17 @@ object ClientRuntimeTypes { val URLPathMiddleware = runtimeSymbol("URLPathMiddleware") val QueryItemMiddleware = runtimeSymbol("QueryItemMiddleware") val HeaderMiddleware = runtimeSymbol("HeaderMiddleware") - val SerializableBodyMiddleware = runtimeSymbol("SerializableBodyMiddleware") val RetryMiddleware = runtimeSymbol("RetryMiddleware") val IdempotencyTokenMiddleware = runtimeSymbol("IdempotencyTokenMiddleware") val NoopHandler = runtimeSymbol("NoopHandler") + val BodyMiddleware = runtimeSymbol("BodyMiddleware") + val PayloadBodyMiddleware = runtimeSymbol("PayloadBodyMiddleware") + val EventStreamBodyMiddleware = runtimeSymbol("EventStreamBodyMiddleware") + val BlobStreamBodyMiddleware = runtimeSymbol("BlobStreamBodyMiddleware") + val BlobBodyMiddleware = runtimeSymbol("BlobBodyMiddleware") + val EnumBodyMiddleware = runtimeSymbol("EnumBodyMiddleware") + val IntEnumBodyMiddleware = runtimeSymbol("IntEnumBodyMiddleware") + val StringBodyMiddleware = runtimeSymbol("StringBodyMiddleware") object Providers { val URLPathProvider = runtimeSymbol("URLPathProvider") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt index 6f99dc769..949eba37f 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ShapeValueGenerator.kt @@ -25,9 +25,7 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.model.traits.StreamingTrait -import software.amazon.smithy.swift.codegen.customtraits.SwiftBoxTrait import software.amazon.smithy.swift.codegen.model.hasTrait -import software.amazon.smithy.swift.codegen.model.recursiveSymbol import software.amazon.smithy.swift.codegen.model.toMemberNames import software.amazon.smithy.swift.codegen.utils.toLowerCamelCase import software.amazon.smithy.utils.StringUtils.lowerCase @@ -47,11 +45,11 @@ class ShapeValueGenerator( * @param shape the shape that will be declared. * @param params parameters to fill the generated shape declaration. */ - fun writeShapeValueInline(writer: SwiftWriter, shape: Shape, params: Node, recursiveMemberWithTrait: Boolean = false) { + fun writeShapeValueInline(writer: SwiftWriter, shape: Shape, params: Node) { val nodeVisitor = ShapeValueNodeVisitor(writer, this, shape) when (shape.type) { - ShapeType.STRUCTURE -> structDecl(writer, shape.asStructureShape().get(), recursiveMemberWithTrait) { + ShapeType.STRUCTURE -> structDecl(writer, shape.asStructureShape().get()) { params.accept(nodeVisitor) } ShapeType.MAP -> mapDecl(writer) { @@ -73,52 +71,13 @@ class ShapeValueGenerator( } } - private fun structDecl(writer: SwiftWriter, shape: StructureShape, recursiveMemberWithTrait: Boolean, block: () -> Unit) { - var symbol = if (recursiveMemberWithTrait) symbolProvider.toSymbol(shape).recursiveSymbol() else symbolProvider.toSymbol(shape) - - /* - The following line changes the generated code from structure instantiation to - Box class instantiation for members with SwiftBoxTrait. - - Changes the instantiation of recursive structure from:- - RecursiveShapesInputOutputNested1( - foo: "Foo1", - nested: RecursiveShapesInputOutputNested2( - bar: "Bar1" - ) - ) - - To:- - RecursiveShapesInputOutputNested1( - foo: "Foo1", - nested: Box( - value: RecursiveShapesInputOutputNested2( - bar: "Bar1" - ) - ) - ) - */ - if (recursiveMemberWithTrait) { - writer.writeInline("\$N(", symbol) - .indent() - .writeInline("\nvalue: ") - - symbol = symbolProvider.toSymbol(shape) - } - /* - The only change with recursive member is that "Box( value: " appended - and the rest of the logic is same as non-recursive members. So, there is no "else" here. - */ + private fun structDecl(writer: SwiftWriter, shape: StructureShape, block: () -> Unit) { + var symbol = symbolProvider.toSymbol(shape) writer.writeInline("\$N(", symbol) .indent() .call { block() } .dedent() .writeInline("\n)") - - if (recursiveMemberWithTrait) { - writer.dedent() - .writeInline("\n)") - } } private fun unionDecl(writer: SwiftWriter, shape: UnionShape, block: () -> Unit) { @@ -236,7 +195,6 @@ class ShapeValueGenerator( val member = currShape.getMember(keyNode.value).orElseThrow { CodegenException("unknown member ${currShape.id}.${keyNode.value}") } - val recursiveMemberWithTrait = member.hasTrait(SwiftBoxTrait::class.java) memberShape = generator.model.expectShape(member.target) val memberName = generator.symbolProvider.toMemberNames(member).second // NOTE - `write()` appends a newline and keeps indentation, @@ -245,7 +203,7 @@ class ShapeValueGenerator( // This is our workaround for the moment to keep indentation but not insert // a newline at the end. writer.writeInline("\n\$L: ", memberName) - generator.writeShapeValueInline(writer, memberShape, valueNode, recursiveMemberWithTrait) + generator.writeShapeValueInline(writer, memberShape, valueNode) if (i < node.members.size - 1) { writer.writeInline(",") } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyReadWriteTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyReadWriteTypes.kt new file mode 100644 index 000000000..d2a0adc01 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyReadWriteTypes.kt @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package software.amazon.smithy.swift.codegen + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.swift.codegen.model.buildSymbol + +/** + * Commonly used runtime types. Provides a single definition of a runtime symbol such that codegen isn't littered + * with inline symbol creation which makes refactoring of the runtime more difficult and error prone. + * + * NOTE: Not all symbols need be added here but it doesn't hurt to define runtime symbols once. + */ +object SmithyReadWriteTypes { + val WritingClosure = runtimeSymbol("WritingClosure") + val DocumentWritingClosure = runtimeSymbol("DocumentWritingClosure") +} + +private fun runtimeSymbol(name: String): Symbol = buildSymbol { + this.name = name + this.namespace = SwiftDependency.SMITHY_READ_WRITE.target + dependency(SwiftDependency.SMITHY_XML) +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyXMLTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyXMLTypes.kt new file mode 100644 index 000000000..42aeea343 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SmithyXMLTypes.kt @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +package software.amazon.smithy.swift.codegen + +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.swift.codegen.model.buildSymbol + +object SmithyXMLTypes { + val XMLReadWrite = runtimeSymbol("XMLReadWrite") + val Writer = runtimeSymbol("Writer") +} + +private fun runtimeSymbol(name: String): Symbol = buildSymbol { + this.name = name + this.namespace = SwiftDependency.SMITHY_XML.target + dependency(SwiftDependency.SMITHY_XML) +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/StructureGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/StructureGenerator.kt index 46d31e8a7..d15ae3a21 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/StructureGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/StructureGenerator.kt @@ -22,7 +22,6 @@ import software.amazon.smithy.swift.codegen.model.getTrait import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.isError import software.amazon.smithy.swift.codegen.model.nestedNamespaceType -import software.amazon.smithy.swift.codegen.model.recursiveSymbol import software.amazon.smithy.swift.codegen.model.toLowerCamelCase import software.amazon.smithy.swift.codegen.utils.errorShapeName import software.amazon.smithy.swift.codegen.utils.toUpperCamelCase @@ -120,13 +119,10 @@ class StructureGenerator( membersSortedByName.forEach { var (memberName, memberSymbol) = memberShapeDataContainer.getOrElse(it) { return@forEach } writer.writeMemberDocs(model, it) - if (it.hasTrait()) { - writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) - memberSymbol = memberSymbol.recursiveSymbol() - } - + val indirect = it.hasTrait() + val indirectOrNot = "@Indirect ".takeIf { indirect } ?: "" writer.writeAvailableAttribute(model, it) - writer.write("public var \$L: \$T", memberName, memberSymbol) + writer.write("\$Lpublic var \$L: \$T", indirectOrNot, memberName, memberSymbol) } } @@ -134,12 +130,13 @@ class StructureGenerator( val hasMembers = membersSortedByName.isNotEmpty() if (hasMembers) { + writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) writer.openBlock("public init(", ")") { for ((index, member) in membersSortedByName.withIndex()) { val (memberName, memberSymbol) = memberShapeDataContainer.getOrElse(member) { Pair(null, null) } if (memberName == null || memberSymbol == null) continue val terminator = if (index == membersSortedByName.size - 1) "" else "," - val symbolToUse = if (member.hasTrait(SwiftBoxTrait::class.java)) memberSymbol.recursiveSymbol() else memberSymbol + val symbolToUse = memberSymbol writer.write("\$L: \$D$terminator", memberName, symbolToUse) } } @@ -230,7 +227,9 @@ class StructureGenerator( val (memberName, memberSymbol) = memberShapeDataContainer.getOrElse(it) { return@forEach } writer.writeMemberDocs(model, it) writer.writeAvailableAttribute(model, it) - writer.write("public internal(set) var \$L: \$D", memberName, memberSymbol) + val targetShape = model.expectShape(it.target) + val boxedOrNot = "@Boxed ".takeIf { targetShape.hasTrait() } + writer.write("\$Lpublic internal(set) var \$L: \$D", boxedOrNot, memberName, memberSymbol) } } writer.write("") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt index d30c300ce..6c0617f06 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftDependency.kt @@ -35,6 +35,22 @@ enum class SwiftDependency( "https://github.com/smithy-lang/smithy-swift", Resources.computeAbsolutePath("smithy-swift", "", "SMITHY_SWIFT_CI_DIR"), "smithy-swift" + ), + SMITHY_READ_WRITE( + "SmithyReadWrite", + "main", + "0.1.0", + "https://github.com/smithy-lang/smithy-swift", + Resources.computeAbsolutePath("smithy-swift", "", "SMITHY_SWIFT_CI_DIR"), + "smithy-swift" + ), + SMITHY_XML( + "SmithyXML", + "main", + "0.1.0", + "https://github.com/smithy-lang/smithy-swift", + Resources.computeAbsolutePath("smithy-swift", "", "SMITHY_SWIFT_CI_DIR"), + "smithy-swift" ); override fun getDependencies(): List { diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index cefe2bb5d..5b5421630 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -52,7 +52,6 @@ import software.amazon.smithy.swift.codegen.integration.middlewares.OperationInp import software.amazon.smithy.swift.codegen.integration.middlewares.OperationInputUrlHostMiddleware import software.amazon.smithy.swift.codegen.integration.middlewares.OperationInputUrlPathMiddleware import software.amazon.smithy.swift.codegen.integration.middlewares.RetryMiddleware -import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.HttpBodyMiddleware import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpHeaderProvider import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpQueryItemProvider import software.amazon.smithy.swift.codegen.integration.middlewares.providers.HttpUrlPathProvider @@ -144,7 +143,6 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { HttpUrlPathProvider.renderUrlPathMiddleware(ctx, operation, httpBindingResolver) HttpHeaderProvider.renderHeaderMiddleware(ctx, operation, httpBindingResolver, defaultTimestampFormat) HttpQueryItemProvider.renderQueryMiddleware(ctx, operation, httpBindingResolver, defaultTimestampFormat) - HttpBodyMiddleware.renderBodyMiddleware(ctx, operation, httpBindingResolver) inputShapesWithHttpBindings.add(inputShapeId) } } @@ -164,10 +162,11 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { .toList() if (httpBodyMembers.isNotEmpty() || shouldRenderEncodableConformance) { ctx.delegator.useShapeWriter(encodeSymbol) { writer -> + val encodableOrNot = encodableProtocol?.let { writer.format(": \$N", it) } ?: "" writer.openBlock( - "extension $symbolName: \$N {", + "extension $symbolName\$L {", "}", - SwiftTypes.Protocols.Encodable, + encodableOrNot, ) { writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) @@ -227,7 +226,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { .build() ctx.delegator.useShapeWriter(encodeSymbol) { writer -> - writer.openBlock("extension \$N: \$N {", "}", symbol, SwiftTypes.Protocols.Codable) { + writer.openBlock("extension \$N: \$N {", "}", symbol, codableProtocol) { writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) val members = shape.members().toList() when (shape) { @@ -248,7 +247,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { unionMembersForCodingKeys.add(0, sdkUnknownMember) generateCodingKeysForMembers(ctx, writer, unionMembersForCodingKeys) writer.write("") - UnionEncodeGeneratorStrategy(ctx, members, writer, defaultTimestampFormat).render() + UnionEncodeGeneratorStrategy(ctx, shape, members, writer, defaultTimestampFormat).render() writer.write("") UnionDecodeGeneratorStrategy(ctx, members, writer, defaultTimestampFormat).render() } @@ -276,7 +275,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { } } writer.write("") - writer.openBlock("extension ${decodeSymbol.name}: \$N {", "}", SwiftTypes.Protocols.Decodable) { + writer.openBlock("extension ${decodeSymbol.name}: \$N {", "}", decodableProtocol) { writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) generateCodingKeysForMembers(ctx, writer, httpBodyMembers) writer.write("") @@ -474,6 +473,10 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { override val operationMiddleware = OperationMiddlewareGenerator() + open val codableProtocol = SwiftTypes.Protocols.Codable + open val encodableProtocol: Symbol? = SwiftTypes.Protocols.Encodable + open val decodableProtocol = SwiftTypes.Protocols.Decodable + protected abstract val defaultTimestampFormat: TimestampFormatTrait.Format protected abstract val codingKeysGenerator: CodingKeysGenerator protected abstract val httpProtocolClientGeneratorFactory: HttpProtocolClientGeneratorFactory diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt index 02d720b50..e66453dbb 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt @@ -53,8 +53,9 @@ open class HttpProtocolClientGenerator( ServiceGenerator.renderOperationDefinition(model, serviceShape, symbolProvider, writer, operationsIndex, it) writer.openBlock("{", "}") { val operationStackName = "operation" + writer.write("let encoder = self.encoder") val generator = MiddlewareExecutionGenerator(ctx, writer, httpBindingResolver, httpProtocolCustomizable, operationMiddleware, operationStackName) - generator.render(it) { writer, labelMemberName -> + generator.render(serviceShape, it) { writer, labelMemberName -> writer.write("throw \$N(\"uri component $labelMemberName unexpectedly nil\")", ClientRuntimeTypes.Core.UnknownClientError) } writer.write("let result = try await $operationStackName.handleMiddleware(context: context, input: input, next: client.getHandler())") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt index b6385824f..ed19ec50c 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt @@ -4,7 +4,6 @@ */ package software.amazon.smithy.swift.codegen.integration -import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.knowledge.OperationIndex import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.shapes.OperationShape @@ -49,13 +48,12 @@ class HttpProtocolTestGenerator( */ fun generateProtocolTests(): Int { val topDownIndex: TopDownIndex = TopDownIndex.of(ctx.model) - val serviceSymbol = ctx.symbolProvider.toSymbol(ctx.service) val operationMiddleware = updateRequestTestMiddleware() var numTests = 0 for (operation in TreeSet(topDownIndex.getContainedOperations(ctx.service).filterNot(::serverOnly))) { - numTests += renderRequestTests(operation, serviceSymbol, operationMiddleware) - numTests += renderResponseTests(operation, serviceSymbol) - numTests += renderErrorTestCases(operation, serviceSymbol) + numTests += renderRequestTests(operation, operationMiddleware) + numTests += renderResponseTests(operation) + numTests += renderErrorTestCases(operation) } return numTests } @@ -90,7 +88,8 @@ class HttpProtocolTestGenerator( return cloned } - private fun renderRequestTests(operation: OperationShape, serviceSymbol: Symbol, operationMiddleware: OperationMiddleware): Int { + private fun renderRequestTests(operation: OperationShape, operationMiddleware: OperationMiddleware): Int { + val serviceSymbol = ctx.symbolProvider.toSymbol(ctx.service) val tempTestCases = operation.getTrait(HttpRequestTestsTrait::class.java) .getOrNull() ?.getTestCasesFor(AppliesTo.CLIENT) @@ -110,6 +109,7 @@ class HttpProtocolTestGenerator( writer.addImport(SwiftDependency.XCTest.target) requestTestBuilder + .ctx(ctx) .writer(writer) .model(ctx.model) .symbolProvider(ctx.symbolProvider) @@ -127,7 +127,8 @@ class HttpProtocolTestGenerator( return requestTestCases.count() } - private fun renderResponseTests(operation: OperationShape, serviceSymbol: Symbol): Int { + private fun renderResponseTests(operation: OperationShape): Int { + val serviceSymbol = ctx.symbolProvider.toSymbol(ctx.service) val tempResponseTests = operation.getTrait(HttpResponseTestsTrait::class.java) .getOrNull() ?.getTestCasesFor(AppliesTo.CLIENT) @@ -145,6 +146,7 @@ class HttpProtocolTestGenerator( writer.addImport(SwiftDependency.XCTest.target) responseTestBuilder + .ctx(ctx) .writer(writer) .model(ctx.model) .symbolProvider(ctx.symbolProvider) @@ -162,7 +164,8 @@ class HttpProtocolTestGenerator( return responseTestCases.count() } - private fun renderErrorTestCases(operation: OperationShape, serviceSymbol: Symbol): Int { + private fun renderErrorTestCases(operation: OperationShape): Int { + val serviceSymbol = ctx.symbolProvider.toSymbol(ctx.service) val operationIndex: OperationIndex = OperationIndex.of(ctx.model) var numTestCases = 0 for (error in operationIndex.getErrors(operation).filterNot(::serverOnly)) { @@ -188,6 +191,7 @@ class HttpProtocolTestGenerator( errorTestBuilder .error(error) + .ctx(ctx) .writer(writer) .model(ctx.model) .symbolProvider(ctx.symbolProvider) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestGenerator.kt index fd4af03a1..4e0d36ea8 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestGenerator.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.swift.codegen.integration import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.protocoltests.traits.HttpMessageTestCase import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.middleware.OperationMiddleware @@ -20,6 +21,7 @@ import software.amazon.smithy.swift.codegen.middleware.OperationMiddleware abstract class HttpProtocolUnitTestGenerator protected constructor(builder: Builder) { + protected val ctx: ProtocolGenerator.GenerationContext = builder.ctx!! protected val symbolProvider: SymbolProvider = builder.symbolProvider!! protected var model: Model = builder.model!! private val testCases: List = builder.testCases!! @@ -71,9 +73,11 @@ protected constructor(builder: Builder) { ) abstract class Builder { + var ctx: ProtocolGenerator.GenerationContext? = null var symbolProvider: SymbolProvider? = null var model: Model? = null var testCases: List? = null + var service: ServiceShape? = null var operation: OperationShape? = null var writer: SwiftWriter? = null var serviceName: String? = null @@ -85,6 +89,7 @@ protected constructor(builder: Builder) { fun symbolProvider(provider: SymbolProvider): Builder = apply { this.symbolProvider = provider } fun model(model: Model): Builder = apply { this.model = model } fun testCases(testCases: List): Builder = apply { this.testCases = testCases } + fun ctx(ctx: ProtocolGenerator.GenerationContext): Builder = apply { this.ctx = ctx } fun operation(operation: OperationShape): Builder = apply { this.operation = operation } fun writer(writer: SwiftWriter): Builder = apply { this.writer = writer } fun serviceName(serviceName: String): Builder = apply { this.serviceName = serviceName } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt index 220b0578e..5dd482384 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt @@ -4,6 +4,10 @@ */ package software.amazon.smithy.swift.codegen.integration +import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait +import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait +import software.amazon.smithy.aws.traits.protocols.RestJson1Trait +import software.amazon.smithy.aws.traits.protocols.RestXmlTrait import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.Shape @@ -19,6 +23,7 @@ import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.hasStreamingMember import software.amazon.smithy.swift.codegen.middleware.MiddlewareStep import software.amazon.smithy.swift.codegen.model.RecursiveShapeBoxer +import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.toUpperCamelCase import software.amazon.smithy.swift.codegen.swiftFunctionParameterIndent @@ -103,11 +108,11 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B val operationStack = "operationStack" writer.write("var $operationStack = OperationStack<$inputSymbol, $outputSymbol, $outputErrorName>(id: \"${test.id}\")") - operationMiddleware.renderMiddleware(writer, operation, operationStack, MiddlewareStep.INITIALIZESTEP) - operationMiddleware.renderMiddleware(writer, operation, operationStack, MiddlewareStep.BUILDSTEP) - operationMiddleware.renderMiddleware(writer, operation, operationStack, MiddlewareStep.SERIALIZESTEP) - operationMiddleware.renderMiddleware(writer, operation, operationStack, MiddlewareStep.FINALIZESTEP) - operationMiddleware.renderMiddleware(writer, operation, operationStack, MiddlewareStep.DESERIALIZESTEP) + operationMiddleware.renderMiddleware(ctx, writer, operation, operationStack, MiddlewareStep.INITIALIZESTEP) + operationMiddleware.renderMiddleware(ctx, writer, operation, operationStack, MiddlewareStep.BUILDSTEP) + operationMiddleware.renderMiddleware(ctx, writer, operation, operationStack, MiddlewareStep.SERIALIZESTEP) + operationMiddleware.renderMiddleware(ctx, writer, operation, operationStack, MiddlewareStep.FINALIZESTEP) + operationMiddleware.renderMiddleware(ctx, writer, operation, operationStack, MiddlewareStep.DESERIALIZESTEP) renderMockDeserializeMiddleware(test, operationStack, inputSymbol, outputSymbol, outputErrorName, inputShape) @@ -155,7 +160,9 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B writer.write("XCTAssertNotNil(expectedHttpBody, \"The expected HttpBody is nil\")") val expectedData = "expectedData" val actualData = "actualData" - writer.openBlock("try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { $expectedData, $actualData in ", "}") { + val isXML = ctx.service.hasTrait() + val isJSON = ctx.service.hasTrait() || ctx.service.hasTrait() || ctx.service.hasTrait() + writer.openBlock("try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: \$L, isJSON: \$L) { $expectedData, $actualData in ", "}", isXML, isJSON) { val httpPayloadShape = inputShape.members().firstOrNull { it.hasTrait(HttpPayloadTrait::class.java) } httpPayloadShape?.let { diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt index af6975535..bfc5c6de0 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt @@ -4,6 +4,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -18,6 +19,7 @@ class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boo override val position = MiddlewarePosition.BEFORE override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentMD5Middleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentMD5Middleware.kt index 9e1028c27..68c1ce7dd 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentMD5Middleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentMD5Middleware.kt @@ -7,6 +7,7 @@ import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -24,7 +25,7 @@ class ContentMD5Middleware( override val position = MiddlewarePosition.BEFORE - override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) { + override fun render(ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String) { if (op.isChecksumRequired()) { val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<$outputShapeName>())", ClientRuntimeTypes.Middleware.ContentMD5Middleware) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentTypeMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentTypeMiddleware.kt index 5a0db8aab..4398e6db2 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentTypeMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentTypeMiddleware.kt @@ -4,6 +4,7 @@ import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -23,6 +24,7 @@ class ContentTypeMiddleware( override val position = MiddlewarePosition.AFTER override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/DeserializeMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/DeserializeMiddleware.kt index b4a63a507..32d030d06 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/DeserializeMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/DeserializeMiddleware.kt @@ -5,6 +5,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -22,6 +23,7 @@ class DeserializeMiddleware( override val position = MiddlewarePosition.AFTER override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt index 3fb6e4025..c54442e2d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.traits.IdempotencyTokenTrait import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -25,7 +26,7 @@ class IdempotencyTokenMiddleware( override val middlewareStep = MiddlewareStep.INITIALIZESTEP override val position = MiddlewarePosition.AFTER - override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) { + override fun render(ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String) { val inputShape = model.expectShape(op.input.get()) val idempotentMember = inputShape.members().firstOrNull { it.hasTrait() } idempotentMember?.let { diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/LoggingMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/LoggingMiddleware.kt index a784d072b..6b4bf248d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/LoggingMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/LoggingMiddleware.kt @@ -10,6 +10,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -27,6 +28,7 @@ class LoggingMiddleware( override val position = MiddlewarePosition.AFTER override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputBodyMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputBodyMiddleware.kt index 6868efddc..70099b22a 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputBodyMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputBodyMiddleware.kt @@ -1,21 +1,36 @@ package software.amazon.smithy.swift.codegen.integration.middlewares +import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait +import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait +import software.amazon.smithy.aws.traits.protocols.RestJson1Trait +import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.BlobShape +import software.amazon.smithy.model.shapes.DocumentShape +import software.amazon.smithy.model.shapes.EnumShape +import software.amazon.smithy.model.shapes.IntEnumShape import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.traits.XmlNameTrait +import software.amazon.smithy.model.shapes.StringShape +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.model.shapes.UnionShape +import software.amazon.smithy.model.traits.HttpPayloadTrait +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.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.DocumentWritingClosureUtils +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.WritingClosureUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable import software.amazon.smithy.swift.codegen.middleware.MiddlewareStep -import software.amazon.smithy.swift.codegen.model.getTrait +import software.amazon.smithy.swift.codegen.model.hasTrait class OperationInputBodyMiddleware( val model: Model, val symbolProvider: SymbolProvider, - val alwaysSendBody: Boolean = false + private val alwaysSendBody: Boolean = false ) : MiddlewareRenderable { override val name = "OperationInputBodyMiddleware" @@ -25,43 +40,155 @@ class OperationInputBodyMiddleware( override val position = MiddlewarePosition.AFTER override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String, ) { + if (!alwaysSendBody && !MiddlewareShapeUtils.hasHttpBody(ctx.model, op)) return + val writingClosureUtils = WritingClosureUtils(ctx, writer) + val documentWritingClosureUtils = DocumentWritingClosureUtils(ctx, writer) val inputShape = MiddlewareShapeUtils.inputShape(model, op) - val inputShapeName = MiddlewareShapeUtils.inputSymbol(symbolProvider, model, op).name - val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name - val xmlName = inputShape.getTrait()?.value + val inputSymbol = symbolProvider.toSymbol(inputShape) + val outputSymbol = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op) + val writerSymbol = documentWritingClosureUtils.writerSymbol() + var payloadShape = inputShape + var keyPath = "\\.self" + var payloadWritingClosure = writingClosureUtils.writingClosure(payloadShape) + var documentWritingClosure = documentWritingClosureUtils.closure(payloadShape) + var isPayloadMember = false + val defaultBody = "\"{}\"".takeIf { ctx.service.hasTrait() || ctx.service.hasTrait() || ctx.service.hasTrait() } ?: "nil" + val payloadMember = inputShape.members().find { it.hasTrait() } + payloadMember?.let { + payloadShape = ctx.model.expectShape(it.target) + val memberName = ctx.symbolProvider.toMemberName(it) + keyPath = writer.format("\\.\$L", memberName) + payloadWritingClosure = writingClosureUtils.writingClosure(it) + documentWritingClosure = documentWritingClosureUtils.closure(it) + isPayloadMember = true + } + val isStreaming = payloadShape.hasTrait() + val payloadSymbol = ctx.symbolProvider.toSymbol(payloadShape) - if (alwaysSendBody) { - if (xmlName != null) { - writer.write( - "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L>(xmlName: \"\$L\"))", - operationStackName, middlewareStep.stringValue(), position.stringValue(), ClientRuntimeTypes.Middleware.SerializableBodyMiddleware, inputShapeName, outputShapeName, xmlName - ) - } else { - writer.write( - "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L>())", - operationStackName, middlewareStep.stringValue(), position.stringValue(), ClientRuntimeTypes.Middleware.SerializableBodyMiddleware, inputShapeName, outputShapeName - ) - } - } else if (MiddlewareShapeUtils.hasHttpBody(model, op)) { - if (MiddlewareShapeUtils.bodyIsHttpPayload(model, op)) { - writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: ${inputShapeName}BodyMiddleware())") - } else { - if (xmlName != null) { - writer.write( - "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L>(xmlName: \"\$L\"))", - operationStackName, middlewareStep.stringValue(), position.stringValue(), ClientRuntimeTypes.Middleware.SerializableBodyMiddleware, inputShapeName, outputShapeName, xmlName - ) + when (payloadShape) { + is UnionShape -> { + if (isStreaming) { + addEventStreamMiddleware(writer, operationStackName, inputSymbol, outputSymbol, payloadSymbol, keyPath, defaultBody) } else { - writer.write( - "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L>())", - operationStackName, middlewareStep.stringValue(), position.stringValue(), ClientRuntimeTypes.Middleware.SerializableBodyMiddleware, inputShapeName, outputShapeName - ) + addAggregateMiddleware(writer, operationStackName, inputSymbol, outputSymbol, payloadSymbol, writerSymbol, documentWritingClosure, payloadWritingClosure, keyPath, defaultBody, isPayloadMember) } } + is StructureShape, is DocumentShape -> { + addAggregateMiddleware(writer, operationStackName, inputSymbol, outputSymbol, payloadSymbol, writerSymbol, documentWritingClosure, payloadWritingClosure, keyPath, defaultBody, isPayloadMember) + } + is BlobShape -> { + addBlobStreamMiddleware(writer, operationStackName, inputSymbol, outputSymbol, keyPath, isStreaming) + } + is EnumShape -> { + addEnumMiddleware(writer, operationStackName, ClientRuntimeTypes.Middleware.EnumBodyMiddleware, inputSymbol, outputSymbol, payloadSymbol, keyPath) + } + is IntEnumShape -> { + addEnumMiddleware(writer, operationStackName, ClientRuntimeTypes.Middleware.IntEnumBodyMiddleware, inputSymbol, outputSymbol, payloadSymbol, keyPath) + } + is StringShape -> { + addStringMiddleware(writer, operationStackName, inputSymbol, outputSymbol, keyPath) + } } } + + private fun addAggregateMiddleware(writer: SwiftWriter, operationStackName: String, inputSymbol: Symbol, outputSymbol: Symbol, payloadSymbol: Symbol, writerSymbol: Symbol, documentWritingClosure: String, payloadWritingClosure: String, keyPath: String, defaultBody: String, isPayloadMember: Boolean) { + if (isPayloadMember) { + addPayloadBodyMiddleware(writer, operationStackName, inputSymbol, outputSymbol, payloadSymbol, writerSymbol, documentWritingClosure, payloadWritingClosure, keyPath, defaultBody) + } else { + addBodyMiddleware(writer, operationStackName, inputSymbol, outputSymbol, writerSymbol, documentWritingClosure, payloadWritingClosure) + } + } + + private fun addBodyMiddleware(writer: SwiftWriter, operationStackName: String, inputSymbol: Symbol, outputSymbol: Symbol, writerSymbol: Symbol, documentWritingClosure: String, payloadWritingClosure: String) { + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$N, \$N, \$N>(documentWritingClosure: \$L, inputWritingClosure: \$L))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.BodyMiddleware, + inputSymbol, + outputSymbol, + writerSymbol, + documentWritingClosure, + payloadWritingClosure, + ) + } + + private fun addPayloadBodyMiddleware(writer: SwiftWriter, operationStackName: String, inputSymbol: Symbol, outputSymbol: Symbol, payloadSymbol: Symbol, writerSymbol: Symbol, documentWritingClosure: String, payloadWritingClosure: String, keyPath: String, defaultBody: String) { + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$N, \$N, \$N, \$N>(documentWritingClosure: \$L, inputWritingClosure: \$L, keyPath: \$L, defaultBody: \$L))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.PayloadBodyMiddleware, + inputSymbol, + outputSymbol, + payloadSymbol, + writerSymbol, + documentWritingClosure, + payloadWritingClosure, + keyPath, + defaultBody + ) + } + + private fun addEventStreamMiddleware(writer: SwiftWriter, operationStackName: String, inputSymbol: Symbol, outputSymbol: Symbol, payloadSymbol: Symbol, keyPath: String, defaultBody: String) { + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$N, \$N, \$N>(keyPath: \$L, defaultBody: \$L))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.EventStreamBodyMiddleware, + inputSymbol, + outputSymbol, + payloadSymbol, + keyPath, + defaultBody + ) + } + + private fun addBlobStreamMiddleware(writer: SwiftWriter, operationStackName: String, inputSymbol: Symbol, outputSymbol: Symbol, keyPath: String, streaming: Boolean) { + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$N, \$N>(keyPath: \$L))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.BlobStreamBodyMiddleware.takeIf { streaming } ?: ClientRuntimeTypes.Middleware.BlobBodyMiddleware, + inputSymbol, + outputSymbol, + keyPath + ) + } + + private fun addEnumMiddleware(writer: SwiftWriter, operationStackName: String, middlewareSymbol: Symbol, inputSymbol: Symbol, outputSymbol: Symbol, payloadSymbol: Symbol, keyPath: String) { + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$N, \$N, \$N>(keyPath: \$L))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + middlewareSymbol, + inputSymbol, + outputSymbol, + payloadSymbol, + keyPath + ) + } + + private fun addStringMiddleware(writer: SwiftWriter, operationStackName: String, inputSymbol: Symbol, outputSymbol: Symbol, keyPath: String) { + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$N, \$N>(keyPath: \$L))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.StringBodyMiddleware, + inputSymbol, + outputSymbol, + keyPath + ) + } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputHeadersMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputHeadersMiddleware.kt index 41afa45d4..b29e713a0 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputHeadersMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputHeadersMiddleware.kt @@ -5,6 +5,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -22,6 +23,7 @@ class OperationInputHeadersMiddleware( override val position = MiddlewarePosition.AFTER override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputQueryItemMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputQueryItemMiddleware.kt index 272807872..fb791dcfc 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputQueryItemMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputQueryItemMiddleware.kt @@ -5,6 +5,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -22,6 +23,7 @@ class OperationInputQueryItemMiddleware( override val position = MiddlewarePosition.AFTER override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlHostMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlHostMiddleware.kt index cb3d2eedb..e07ddb0cf 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlHostMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlHostMiddleware.kt @@ -7,6 +7,7 @@ import software.amazon.smithy.model.traits.EndpointTrait import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.EndpointTraitConstructor +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -27,6 +28,7 @@ class OperationInputUrlHostMiddleware( override val position = MiddlewarePosition.AFTER override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt index 67bb79cb1..e5109b0de 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt @@ -5,6 +5,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -23,6 +24,7 @@ class OperationInputUrlPathMiddleware( override val position = MiddlewarePosition.AFTER override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RequestTestEndpointResolverMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RequestTestEndpointResolverMiddleware.kt index 47dff6fd5..eb167b6db 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RequestTestEndpointResolverMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RequestTestEndpointResolverMiddleware.kt @@ -5,6 +5,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -14,7 +15,7 @@ class RequestTestEndpointResolverMiddleware(private val model: Model, private va override val name = "RequestTestEndpointResolver" override val middlewareStep = MiddlewareStep.BUILDSTEP override val position = MiddlewarePosition.AFTER - override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) { + override fun render(ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String) { val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name val outputErrorShapeName = MiddlewareShapeUtils.outputErrorSymbolName(op) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RetryMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RetryMiddleware.kt index 46053e6d7..d9b28a28d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RetryMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RetryMiddleware.kt @@ -11,6 +11,7 @@ import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable @@ -28,7 +29,7 @@ class RetryMiddleware( override val position = MiddlewarePosition.AFTER - override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) { + override fun render(ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String) { val output = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op) val outputError = MiddlewareShapeUtils.outputErrorSymbol(op) writer.write( diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/handlers/HttpBodyMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/handlers/HttpBodyMiddleware.kt deleted file mode 100644 index 6174937b8..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/handlers/HttpBodyMiddleware.kt +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package software.amazon.smithy.swift.codegen.integration.middlewares.handlers - -import software.amazon.smithy.aws.traits.protocols.RestXmlTrait -import software.amazon.smithy.codegen.core.CodegenException -import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.model.knowledge.HttpBinding -import software.amazon.smithy.model.shapes.OperationShape -import software.amazon.smithy.model.shapes.ShapeType -import software.amazon.smithy.model.traits.EnumTrait -import software.amazon.smithy.model.traits.StreamingTrait -import software.amazon.smithy.model.traits.XmlNameTrait -import software.amazon.smithy.swift.codegen.ClientRuntimeTypes -import software.amazon.smithy.swift.codegen.Middleware -import software.amazon.smithy.swift.codegen.MiddlewareGenerator -import software.amazon.smithy.swift.codegen.SwiftDependency -import software.amazon.smithy.swift.codegen.SwiftWriter -import software.amazon.smithy.swift.codegen.integration.HttpBindingDescriptor -import software.amazon.smithy.swift.codegen.integration.HttpBindingResolver -import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator -import software.amazon.smithy.swift.codegen.integration.steps.OperationSerializeStep -import software.amazon.smithy.swift.codegen.model.getTrait -import software.amazon.smithy.swift.codegen.model.hasTrait - -class HttpBodyMiddleware( - private val writer: SwiftWriter, - private val ctx: ProtocolGenerator.GenerationContext, - inputSymbol: Symbol, - outputSymbol: Symbol, - private val outputErrorSymbol: Symbol, - private val requestBindings: List -) : Middleware(writer, inputSymbol, OperationSerializeStep(inputSymbol, outputSymbol, outputErrorSymbol)) { - - override val typeName = "${inputSymbol.name}BodyMiddleware" - companion object { - fun renderBodyMiddleware( - ctx: ProtocolGenerator.GenerationContext, - op: OperationShape, - httpBindingResolver: HttpBindingResolver - ) { - if (MiddlewareShapeUtils.hasHttpBody(ctx.model, op) && MiddlewareShapeUtils.bodyIsHttpPayload(ctx.model, op)) { - val inputSymbol = MiddlewareShapeUtils.inputSymbol(ctx.symbolProvider, ctx.model, op) - val outputSymbol = MiddlewareShapeUtils.outputSymbol(ctx.symbolProvider, ctx.model, op) - val outputErrorSymbol = MiddlewareShapeUtils.outputErrorSymbol(op) - val rootNamespace = MiddlewareShapeUtils.rootNamespace(ctx.settings) - val requestBindings = httpBindingResolver.requestBindings(op) - val headerMiddlewareSymbol = Symbol.builder() - .definitionFile("./$rootNamespace/models/${inputSymbol.name}+BodyMiddleware.swift") - .name(inputSymbol.name) - .build() - ctx.delegator.useShapeWriter(headerMiddlewareSymbol) { writer -> - writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) - - val bodyMiddleware = HttpBodyMiddleware(writer, ctx, inputSymbol, outputSymbol, outputErrorSymbol, requestBindings) - MiddlewareGenerator(writer, bodyMiddleware).generate() - } - } - } - } - override fun generateMiddlewareClosure() { - renderEncodedBody() - } - - override fun generateInit() { - writer.write("public init() {}") - } - - private fun renderEncodedBody() { - val httpPayload = requestBindings.firstOrNull { it.location == HttpBinding.Location.PAYLOAD } - if (httpPayload != null) { - renderExplicitPayload(httpPayload) - } - } - - private fun renderExplicitPayload(binding: HttpBindingDescriptor) { - val memberName = ctx.symbolProvider.toMemberName(binding.member) - val target = ctx.model.expectShape(binding.member.target) - val dataDeclaration = "${memberName}Data" - val bodyDeclaration = "${memberName}Body" - - when (target.type) { - ShapeType.BLOB -> { - val isBinaryStream = - ctx.model.getShape(binding.member.target).get().hasTrait() - writer.openBlock("if let $memberName = input.operationInput.$memberName {", "}") { - if (!isBinaryStream) { - writer.write("let $dataDeclaration = \$L", memberName) - } - renderEncodedBodyAddedToRequest(memberName, bodyDeclaration, dataDeclaration, isBinaryStream) - } - } - ShapeType.STRING -> { - val contents = if (target.hasTrait()) "$memberName.rawValue" else memberName - writer.openBlock("if let $memberName = input.operationInput.$memberName {", "}") { - writer.write("let $dataDeclaration = \$L.data(using: .utf8)", contents) - renderEncodedBodyAddedToRequest(memberName, bodyDeclaration, dataDeclaration) - } - } - ShapeType.ENUM -> { - val contents = "$memberName.rawValue" - writer.openBlock("if let $memberName = input.operationInput.$memberName {", "}") { - writer.write("let $dataDeclaration = \$L.data(using: .utf8)", contents) - renderEncodedBodyAddedToRequest(memberName, bodyDeclaration, dataDeclaration) - } - } - ShapeType.STRUCTURE, ShapeType.UNION -> { - // delegate to the member encode function - writer.openBlock("do {", "} catch let err {") { - writer.write("let encoder = context.getEncoder()") - writer.openBlock("if let $memberName = input.operationInput.$memberName {", "} else {") { - - val xmlNameTrait = binding.member.getTrait() ?: target.getTrait() - if (ctx.protocol == RestXmlTrait.ID && xmlNameTrait != null) { - val xmlName = xmlNameTrait.value - writer.write("let xmlEncoder = encoder as! XMLEncoder") - if (target.hasTrait() && target.isUnionShape) { - writer.openBlock("guard let messageEncoder = context.getMessageEncoder() else {", "}") { - writer.write("fatalError(\"Message encoder is required for streaming payload\")") - } - writer.openBlock("guard let messageSigner = context.getMessageSigner() else {", "}") { - writer.write("fatalError(\"Message signer is required for streaming payload\")") - } - writer.write( - "let encoderStream = \$L(stream: $memberName, messageEncoder: messageEncoder, requestEncoder: xmlEncoder, messageSinger: messageSigner)", - ClientRuntimeTypes.EventStream.MessageEncoderStream - ) - writer.write("input.builder.withBody(.stream(encoderStream))") - } else { - writer.write("let $dataDeclaration = try xmlEncoder.encode(\$L, withRootKey: \"\$L\")", memberName, xmlName) - renderEncodedBodyAddedToRequest(memberName, bodyDeclaration, dataDeclaration) - } - } else { - if (target.hasTrait() && target.isUnionShape) { - writer.openBlock("guard let messageEncoder = context.getMessageEncoder() else {", "}") { - writer.write("fatalError(\"Message encoder is required for streaming payload\")") - } - writer.openBlock("guard let messageSigner = context.getMessageSigner() else {", "}") { - writer.write("fatalError(\"Message signer is required for streaming payload\")") - } - writer.write( - "let encoderStream = \$L(stream: $memberName, messageEncoder: messageEncoder, requestEncoder: encoder, messageSinger: messageSigner)", - ClientRuntimeTypes.EventStream.MessageEncoderStream - ) - writer.write("input.builder.withBody(.stream(encoderStream))") - } else { - writer.write("let $dataDeclaration = try encoder.encode(\$L)", memberName) - renderEncodedBodyAddedToRequest(memberName, bodyDeclaration, dataDeclaration) - } - } - } - writer.indent() - writer.openBlock("if encoder is JSONEncoder {", "}") { - writer.write("// Encode an empty body as an empty structure in JSON") - writer.write("let \$L = \"{}\".data(using: .utf8)!", dataDeclaration) - renderEncodedBodyAddedToRequest(memberName, bodyDeclaration, dataDeclaration) - } - writer.dedent() - writer.write("}") - } - renderErrorCase() - } - ShapeType.DOCUMENT -> { - writer.openBlock("do {", "} catch let err {") { - writer.openBlock("if let $memberName = input.operationInput.$memberName {", "}") { - writer.write("let encoder = context.getEncoder()") - writer.write("let $dataDeclaration = try encoder.encode(\$L)", memberName) - renderEncodedBodyAddedToRequest(memberName, bodyDeclaration, dataDeclaration) - } - } - renderErrorCase() - } - else -> throw CodegenException("member shape ${binding.member} serializer not implemented yet") - } - } - - private fun renderEncodedBodyAddedToRequest( - memberName: String, - bodyDeclaration: String, - dataDeclaration: String, - isBinaryStream: Boolean = false - ) { - if (isBinaryStream) { - writer.write("let $bodyDeclaration = \$N(byteStream: $memberName)", ClientRuntimeTypes.Http.HttpBody) - } else { - writer.write("let $bodyDeclaration = \$N.data($dataDeclaration)", ClientRuntimeTypes.Http.HttpBody) - } - writer.write("input.builder.withBody($bodyDeclaration)") - } - - private fun renderErrorCase() { - writer.indent() - writer.write("throw \$N(err.localizedDescription)", ClientRuntimeTypes.Core.UnknownClientError) - writer.dedent() - writer.write("}") - } -} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/DynamicNodeDecodingGeneratorStrategy.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/DynamicNodeDecodingGeneratorStrategy.kt index 1640ffc0b..f60731d4c 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/DynamicNodeDecodingGeneratorStrategy.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/DynamicNodeDecodingGeneratorStrategy.kt @@ -5,8 +5,11 @@ package software.amazon.smithy.swift.codegen.integration.serde +import software.amazon.smithy.aws.traits.protocols.RestXmlTrait import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.traits.XmlAttributeTrait import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.isInHttpBody import software.amazon.smithy.swift.codegen.integration.serde.xml.DynamicNodeDecodingXMLGenerator class DynamicNodeDecodingGeneratorStrategy( @@ -24,3 +27,14 @@ class DynamicNodeDecodingGeneratorStrategy( return isRestXmlProtocolAndHasXmlAttributesInMembers(ctx, shape) } } + +fun isRestXmlProtocolAndHasXmlAttributesInMembers(ctx: ProtocolGenerator.GenerationContext, shape: Shape): Boolean { + val isRestXML = ctx.protocol == RestXmlTrait.ID + if (isRestXML) { + return shape.members() + .filter { it.isInHttpBody() } + .filter { it.hasTrait(XmlAttributeTrait::class.java) } + .isNotEmpty() + } + return false +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/DynamicNodeEncodingGeneratorStrategy.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/DynamicNodeEncodingGeneratorStrategy.kt deleted file mode 100644 index b4e168bca..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/DynamicNodeEncodingGeneratorStrategy.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package software.amazon.smithy.swift.codegen.integration.serde - -import software.amazon.smithy.aws.traits.protocols.RestXmlTrait -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.traits.XmlAttributeTrait -import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator -import software.amazon.smithy.swift.codegen.integration.isInHttpBody -import software.amazon.smithy.swift.codegen.integration.serde.xml.DynamicNodeEncodingXMLGenerator - -class DynamicNodeEncodingGeneratorStrategy( - private val ctx: ProtocolGenerator.GenerationContext, - private val shape: Shape, - private val xmlNamespaces: Set -) { - fun renderIfNeeded() { - val hasXMLAttributes = isRestXmlProtocolAndHasXmlAttributesInMembers(ctx, shape) - if (hasXMLAttributes || xmlNamespaces.isNotEmpty()) { - DynamicNodeEncodingXMLGenerator(ctx, shape, xmlNamespaces).render() - } - } -} - -fun isRestXmlProtocolAndHasXmlAttributesInMembers(ctx: ProtocolGenerator.GenerationContext, shape: Shape): Boolean { - val isRestXML = ctx.protocol == RestXmlTrait.ID - if (isRestXML) { - return shape.members() - .filter { it.isInHttpBody() } - .filter { it.hasTrait(XmlAttributeTrait::class.java) } - .isNotEmpty() - } - return false -} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/UnionEncodeGeneratorStrategy.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/UnionEncodeGeneratorStrategy.kt index 316415995..a7eaf27a9 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/UnionEncodeGeneratorStrategy.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/UnionEncodeGeneratorStrategy.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.swift.codegen.integration.serde import software.amazon.smithy.aws.traits.protocols.RestXmlTrait import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator @@ -15,6 +16,7 @@ import software.amazon.smithy.swift.codegen.integration.serde.xml.UnionEncodeXML class UnionEncodeGeneratorStrategy( private val ctx: ProtocolGenerator.GenerationContext, + private val union: UnionShape, private val members: List, private val writer: SwiftWriter, private val defaultTimestampFormat: TimestampFormatTrait.Format @@ -22,7 +24,7 @@ class UnionEncodeGeneratorStrategy( fun render() { when (ctx.protocol) { RestXmlTrait.ID -> { - UnionEncodeXMLGenerator(ctx, members, writer, defaultTimestampFormat).render() + UnionEncodeXMLGenerator(ctx, union, members, writer).render() } else -> { UnionEncodeGenerator(ctx, members, writer, defaultTimestampFormat).render() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeDecodeGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeDecodeGenerator.kt index ffcb75de1..2f44b5077 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeDecodeGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeDecodeGenerator.kt @@ -16,7 +16,6 @@ import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftTypes import software.amazon.smithy.swift.codegen.SwiftWriter -import software.amazon.smithy.swift.codegen.customtraits.SwiftBoxTrait import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.serde.MemberShapeDecodeGeneratable import software.amazon.smithy.swift.codegen.integration.serde.TimestampDecodeGenerator @@ -24,7 +23,6 @@ import software.amazon.smithy.swift.codegen.integration.serde.TimestampHelpers import software.amazon.smithy.swift.codegen.model.defaultValue import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.isBoxed -import software.amazon.smithy.swift.codegen.model.recursiveSymbol import software.amazon.smithy.swift.codegen.model.toMemberNames import software.amazon.smithy.swift.codegen.removeSurroundingBackticks @@ -55,9 +53,6 @@ abstract class MemberShapeDecodeGenerator( fun writeDecodeForPrimitive(shape: Shape, member: MemberShape, containerName: String, ignoreDefaultValues: Boolean = false) { var symbol = ctx.symbolProvider.toSymbol(member) val memberName = ctx.symbolProvider.toMemberNames(member).second - if (member.hasTrait(SwiftBoxTrait::class.java)) { - symbol = symbol.recursiveSymbol() - } val defaultValue = symbol.defaultValue() val decodeVerb = if (symbol.isBoxed() || !defaultValue.isNullOrEmpty()) "decodeIfPresent" else "decode" val decodedMemberName = "${memberName}Decoded" diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeEncodeGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeEncodeGenerator.kt index 80663bad2..913e8f2da 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeEncodeGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeEncodeGenerator.kt @@ -20,7 +20,6 @@ import software.amazon.smithy.model.traits.StreamingTrait import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter -import software.amazon.smithy.swift.codegen.customtraits.SwiftBoxTrait import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.serde.MemberShapeEncodeGeneratable import software.amazon.smithy.swift.codegen.integration.serde.TimestampEncodeGenerator @@ -48,11 +47,6 @@ abstract class MemberShapeEncodeGenerator( special types like enum, timestamp, blob */ private fun getShapeExtension(shape: Shape, memberName: String, isBoxed: Boolean, isUnwrapped: Boolean = true): String { - val isRecursiveMember = when (shape) { - is MemberShape -> shape.hasTrait(SwiftBoxTrait::class.java) - else -> false - } - // target shape type to deserialize is either the shape itself or member.target val target = when (shape) { is MemberShape -> ctx.model.expectShape(shape.target) @@ -63,7 +57,7 @@ abstract class MemberShapeEncodeGenerator( return when (target) { is StringShape -> if (target.hasTrait()) "$memberNameOptional.rawValue" else memberName is BlobShape -> if (target.hasTrait()) "$memberNameOptional" else "$memberNameOptional.base64EncodedString()" - else -> if (isRecursiveMember) "$memberName.value" else memberName + else -> memberName } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/readwrite/DocumentWritingClosureUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/readwrite/DocumentWritingClosureUtils.kt new file mode 100644 index 000000000..e6c44bbdc --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/readwrite/DocumentWritingClosureUtils.kt @@ -0,0 +1,85 @@ +package software.amazon.smithy.swift.codegen.integration.serde.readwrite + +import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait +import software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait +import software.amazon.smithy.aws.traits.protocols.RestXmlTrait +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.swift.codegen.ClientRuntimeTypes +import software.amazon.smithy.swift.codegen.SmithyXMLTypes +import software.amazon.smithy.swift.codegen.SwiftDependency +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.serde.xml.NodeInfoUtils +import software.amazon.smithy.swift.codegen.model.hasTrait + +class DocumentWritingClosureUtils( + val ctx: ProtocolGenerator.GenerationContext, + val writer: SwiftWriter +) { + private enum class AwsProtocol { + XML, FORM_URL, JSON + } + + private val awsProtocol: AwsProtocol + get() = if (ctx.service.hasTrait()) { + AwsProtocol.XML + } else if (ctx.service.hasTrait() || ctx.service.hasTrait()) { + AwsProtocol.FORM_URL + } else { + AwsProtocol.JSON + } + + fun closure(memberShape: MemberShape): String { + val rootNodeInfo = NodeInfoUtils(ctx, writer).nodeInfo(memberShape, true) + return closure(rootNodeInfo) + } + + fun closure(valueShape: Shape): String { + val rootNodeInfo = NodeInfoUtils(ctx, writer).nodeInfo(valueShape) + return closure(rootNodeInfo) + } + private fun closure(rootNodeInfo: String): String { + when (awsProtocol) { + AwsProtocol.XML -> { + return writer.format("\$N.documentWritingClosure(rootNodeInfo: \$L)", readWriteSymbol(), rootNodeInfo) + } + AwsProtocol.FORM_URL, AwsProtocol.JSON -> { + return writer.format("\$N.documentWritingClosure(encoder: encoder)", readWriteSymbol()) + } + } + } + + fun writerSymbol(): Symbol { + when (awsProtocol) { + AwsProtocol.XML -> { + writer.addImport(SwiftDependency.SMITHY_XML.target) + return SmithyXMLTypes.Writer + } + AwsProtocol.FORM_URL -> { + return ClientRuntimeTypes.Serde.FormURLWriter + } + AwsProtocol.JSON -> { + return ClientRuntimeTypes.Serde.JSONWriter + } + } + } + + private fun readWriteSymbol(): Symbol { + when (awsProtocol) { + AwsProtocol.XML -> { + writer.addImport(SwiftDependency.SMITHY_XML.target) + return SmithyXMLTypes.XMLReadWrite + } + AwsProtocol.FORM_URL -> { + writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) + return ClientRuntimeTypes.Serde.FormURLReadWrite + } + AwsProtocol.JSON -> { + writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) + return ClientRuntimeTypes.Serde.JSONReadWrite + } + } + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/readwrite/WritingClosureUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/readwrite/WritingClosureUtils.kt new file mode 100644 index 000000000..6e3b71add --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/readwrite/WritingClosureUtils.kt @@ -0,0 +1,86 @@ +package software.amazon.smithy.swift.codegen.integration.serde.readwrite + +import software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait +import software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait +import software.amazon.smithy.aws.traits.protocols.AwsQueryTrait +import software.amazon.smithy.aws.traits.protocols.Ec2QueryTrait +import software.amazon.smithy.aws.traits.protocols.RestJson1Trait +import software.amazon.smithy.model.shapes.ListShape +import software.amazon.smithy.model.shapes.MapShape +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.TimestampShape +import software.amazon.smithy.model.traits.TimestampFormatTrait +import software.amazon.smithy.model.traits.XmlFlattenedTrait +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.serde.json.TimestampUtils +import software.amazon.smithy.swift.codegen.integration.serde.xml.NodeInfoUtils +import software.amazon.smithy.swift.codegen.model.getTrait +import software.amazon.smithy.swift.codegen.model.hasTrait + +class WritingClosureUtils( + val ctx: ProtocolGenerator.GenerationContext, + val writer: SwiftWriter +) { + + private val nodeInfoUtils = NodeInfoUtils(ctx, writer) + + fun writingClosure(member: MemberShape): String { + val target = ctx.model.expectShape(member.target) + val memberTimestampFormatTrait = member.getTrait() + return writingClosure(target, memberTimestampFormatTrait) + } + + fun writingClosure(shape: Shape): String { + return writingClosure(shape, null) + } + + private fun writingClosure(shape: Shape, memberTimestampFormatTrait: TimestampFormatTrait? = null): String { + when { + ctx.service.hasTrait() || + ctx.service.hasTrait() || + ctx.service.hasTrait() -> { + return "JSONReadWrite.writingClosure()" + } + ctx.service.hasTrait() || ctx.service.hasTrait() -> { + return "FormURLReadWrite.writingClosure()" + } + } + return when (shape) { + is MapShape -> { + val keyNodeInfo = nodeInfoUtils.nodeInfo(shape.key) + val valueNodeInfo = nodeInfoUtils.nodeInfo(shape.value) + val valueWriter = writingClosure(shape.value) + val isFlattened = shape.hasTrait() + writer.format( + "SmithyXML.mapWritingClosure(valueWritingClosure: \$L, keyNodeInfo: \$L, valueNodeInfo: \$L, isFlattened: \$L)", + valueWriter, + keyNodeInfo, + valueNodeInfo, + isFlattened + ) + } + is ListShape -> { + val memberNodeInfo = nodeInfoUtils.nodeInfo(shape.member) + val memberWriter = writingClosure(shape.member) + val isFlattened = shape.hasTrait() + writer.format( + "SmithyXML.listWritingClosure(memberWritingClosure: \$L, memberNodeInfo: \$L, isFlattened: \$L)", + memberWriter, + memberNodeInfo, + isFlattened + ) + } + is TimestampShape -> { + writer.format( + "SmithyXML.timestampWritingClosure(format: \$L)", + TimestampUtils.timestampFormat(memberTimestampFormatTrait, shape) + ) + } + else -> { + writer.format("\$N.writingClosure(_:to:)", ctx.symbolProvider.toSymbol(shape)) + } + } + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/DynamicNodeEncodingXMLGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/DynamicNodeEncodingXMLGenerator.kt deleted file mode 100644 index 306b64538..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/DynamicNodeEncodingXMLGenerator.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package software.amazon.smithy.swift.codegen.integration.serde.xml - -import software.amazon.smithy.codegen.core.Symbol -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.traits.XmlAttributeTrait -import software.amazon.smithy.swift.codegen.ClientRuntimeTypes -import software.amazon.smithy.swift.codegen.SwiftDependency -import software.amazon.smithy.swift.codegen.SwiftTypes -import software.amazon.smithy.swift.codegen.SwiftWriter -import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator -import software.amazon.smithy.swift.codegen.integration.isInHttpBody -import software.amazon.smithy.swift.codegen.integration.serde.xml.trait.XMLNameTraitGenerator - -class DynamicNodeEncodingXMLGenerator( - private val ctx: ProtocolGenerator.GenerationContext, - private val shape: Shape, - private val xmlNamespaces: Set -) { - fun render() { - val symbol = ctx.symbolProvider.toSymbol(shape) - val symbolName = symbol.name - val rootNamespace = ctx.settings.moduleName - val encodeSymbol = Symbol.builder() - .definitionFile("./$rootNamespace/models/$symbolName+DynamicNodeEncoding.swift") - .name(symbolName) - .build() - ctx.delegator.useShapeWriter(encodeSymbol) { writer -> - writer.openBlock("extension \$N: \$N {", "}", symbol, ClientRuntimeTypes.Serde.DynamicNodeEncoding) { - writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) - renderNodeEncodingConformance(writer) - } - } - } - - private fun renderNodeEncodingConformance(writer: SwiftWriter) { - writer.openBlock("public static func nodeEncoding(for key: \$N) -> \$N {", "}", SwiftTypes.CodingKey, ClientRuntimeTypes.Serde.NodeEncoding) { - renderNamespaces(xmlNamespaces, writer) - renderAttributes(writer) - writer.write("return .element") - } - } - - private fun renderNamespaces(namespaces: Set, writer: SwiftWriter) { - renderGenericAttributeElementBlock(writer, "xmlNamespaceValues", namespaces) - } - - private fun renderAttributes(writer: SwiftWriter) { - val httpBodyMembers = shape.members() - .filter { it.isInHttpBody() } - .filter { it.hasTrait(XmlAttributeTrait::class.java) } - .map { XMLNameTraitGenerator.construct(it, ctx.symbolProvider.toMemberName(it)).toString() } - .toSet() - renderGenericAttributeElementBlock(writer, "codingKeys", httpBodyMembers) - } - - private fun renderGenericAttributeElementBlock(writer: SwiftWriter, variableName: String, attributes: Set) { - if (attributes.isEmpty()) { - return - } - writer.openBlock("let $variableName = [", "]") { - val itemIndividuallyQuoted = attributes.map { "\"${it}\"" }.sorted() - writer.write(itemIndividuallyQuoted.joinToString(", \n")) - } - writer.openBlock("if let key = key as? \$N {", "}", ClientRuntimeTypes.Serde.Key) { - writer.openBlock("if $variableName.contains(key.stringValue) {", "}") { - writer.write("return .attribute") - } - } - } -} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeDecodeXMLGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeDecodeXMLGenerator.kt index 18b5e25bf..9eb322834 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeDecodeXMLGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeDecodeXMLGenerator.kt @@ -30,10 +30,8 @@ import software.amazon.smithy.swift.codegen.integration.serde.TimestampDecodeGen import software.amazon.smithy.swift.codegen.integration.serde.TimestampHelpers import software.amazon.smithy.swift.codegen.integration.serde.xml.collection.CollectionMemberCodingKey import software.amazon.smithy.swift.codegen.integration.serde.xml.collection.MapKeyValue -import software.amazon.smithy.swift.codegen.model.getTrait import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.isBoxed -import software.amazon.smithy.swift.codegen.model.recursiveSymbol import software.amazon.smithy.swift.codegen.removeSurroundingBackticks abstract class MemberShapeDecodeXMLGenerator( @@ -267,9 +265,6 @@ abstract class MemberShapeDecodeXMLGenerator( val memberName = ctx.symbolProvider.toMemberName(member) val memberNameUnquoted = memberName.removeSurrounding("`", "`") var memberTargetSymbol = ctx.symbolProvider.toSymbol(memberTarget) - if (member.hasTrait(SwiftBoxTrait::class.java)) { - memberTargetSymbol = memberTargetSymbol.recursiveSymbol() - } val decodedMemberName = "${memberName}Decoded" writer.openBlock("if $containerName.contains(.$memberNameUnquoted) {", "} else {") { @@ -296,9 +291,6 @@ abstract class MemberShapeDecodeXMLGenerator( val memberName = ctx.symbolProvider.toMemberName(member) val memberNameUnquoted = memberName.removeSurrounding("`", "`") var memberTargetSymbol = ctx.symbolProvider.toSymbol(member) - if (member.hasTrait(SwiftBoxTrait::class.java)) { - memberTargetSymbol = memberTargetSymbol.recursiveSymbol() - } val decodeVerb = if (memberTargetSymbol.isBoxed() && !isUnion || (member.hasTrait())) "decodeIfPresent" else "decode" val decodedMemberName = "${memberNameUnquoted}Decoded" diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeEncodeXMLGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeEncodeXMLGenerator.kt index 870aa1afe..b3f415b50 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeEncodeXMLGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeEncodeXMLGenerator.kt @@ -4,382 +4,137 @@ */ package software.amazon.smithy.swift.codegen.integration.serde.json -import software.amazon.smithy.model.shapes.CollectionShape +import software.amazon.smithy.model.shapes.ListShape import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.MemberShape -import software.amazon.smithy.model.shapes.SetShape import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.TimestampShape +import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.model.traits.XmlFlattenedTrait -import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator -import software.amazon.smithy.swift.codegen.integration.serde.MemberShapeEncodeConstants import software.amazon.smithy.swift.codegen.integration.serde.MemberShapeEncodeGeneratable -import software.amazon.smithy.swift.codegen.integration.serde.TimestampEncodeGenerator -import software.amazon.smithy.swift.codegen.integration.serde.TimestampHelpers -import software.amazon.smithy.swift.codegen.integration.serde.getDefaultValueOfShapeType -import software.amazon.smithy.swift.codegen.integration.serde.xml.trait.XMLNameTraitGenerator -import software.amazon.smithy.swift.codegen.integration.serde.xml.trait.XMLNamespaceTraitGenerator -import software.amazon.smithy.swift.codegen.model.isBoxed -import software.amazon.smithy.swift.codegen.removeSurroundingBackticks +import software.amazon.smithy.swift.codegen.integration.serde.readwrite.WritingClosureUtils +import software.amazon.smithy.swift.codegen.integration.serde.xml.NodeInfoUtils +import software.amazon.smithy.swift.codegen.model.getTrait +import software.amazon.smithy.swift.codegen.model.hasTrait abstract class MemberShapeEncodeXMLGenerator( private val ctx: ProtocolGenerator.GenerationContext, private val writer: SwiftWriter, - private val defaultTimestampFormat: TimestampFormatTrait.Format ) : MemberShapeEncodeGeneratable { - val xmlNamespaces = mutableSetOf() + private val writingClosureUtils = WritingClosureUtils(ctx, writer) - fun renderListMember( - member: MemberShape, - memberTarget: CollectionShape, - containerName: String - ) { - val originalMemberName = member.memberName - val memberName = ctx.symbolProvider.toMemberName(member) - val resolvedMemberName = XMLNameTraitGenerator.construct(member, originalMemberName) - val nestedContainer = "${memberName.removeSurroundingBackticks()}Container" - if (member.hasTrait(XmlFlattenedTrait::class.java)) { - writer.openBlock("if $memberName.isEmpty {", "} else {") { - writer.write("var $nestedContainer = $containerName.nestedUnkeyedContainer(forKey: \$N(\"$resolvedMemberName\"))", ClientRuntimeTypes.Serde.Key) - writer.write("try $nestedContainer.encodeNil()") - } - writer.indent() - renderFlattenedListMemberItems(memberName, member, memberTarget, containerName) - writer.dedent() - writer.write("}") - } else { - writer.write("var $nestedContainer = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"$resolvedMemberName\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - XMLNamespaceTraitGenerator.construct(member)?.render(writer, nestedContainer)?.appendKey(xmlNamespaces) - renderListMemberItems(memberName, memberTarget, nestedContainer) - } - } - - private fun renderListMemberItems( - memberName: String, - memberTarget: CollectionShape, - containerName: String, - level: Int = 0 - ) { - val nestedMember = memberTarget.member - val nestedMemberResolvedName = XMLNameTraitGenerator.construct(nestedMember, "member").toString() + private val nodeInfoUtils = NodeInfoUtils(ctx, writer) - val nestedMemberTarget = ctx.model.expectShape(memberTarget.member.target) - val nestedMemberTargetName = "${nestedMemberTarget.id.name.toLowerCase()}$level" - writer.openBlock("for $nestedMemberTargetName in $memberName {", "}") { - when (nestedMemberTarget) { - is CollectionShape -> { - renderNestedListEntryMember(nestedMemberTargetName, nestedMemberTarget, nestedMember, nestedMemberResolvedName, containerName, level) - } - is MapShape -> { - val nestedContainerName = "${memberName.removeSurroundingBackticks()}Container$level" - writer.write("var $nestedContainerName = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"${nestedMemberResolvedName}\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - writer.openBlock("if let $nestedMemberTargetName = $nestedMemberTargetName {", "}") { - renderWrappedMapMemberItem(nestedMemberTargetName, nestedMemberTarget, nestedContainerName, level) - } - } - is TimestampShape -> { - val codingKey = writer.format("\$L(\"\$L\")", ClientRuntimeTypes.Serde.Key, nestedMemberResolvedName) - TimestampEncodeGenerator( - containerName, - nestedMemberTargetName, - codingKey, - TimestampHelpers.getTimestampFormat(nestedMember, nestedMemberTarget, defaultTimestampFormat) - ).generate(writer) - } - else -> { - val nestedMemberNamespaceTraitGenerator = XMLNamespaceTraitGenerator.construct(nestedMember) - val nestedContainerName = "${memberName.removeSurroundingBackticks()}Container$level" - renderItem(writer, nestedMemberNamespaceTraitGenerator, nestedContainerName, containerName, nestedMemberTargetName, nestedMemberTarget, nestedMemberResolvedName) - } + fun writeMember(memberShape: MemberShape, unionMember: Boolean) { + val prefix = "value.".takeIf { !unionMember } ?: "" + val targetShape = ctx.model.expectShape(memberShape.target) + when (targetShape) { + is StructureShape, is UnionShape -> { + writeStructureOrUnionMember(memberShape, prefix) } - } - } - - private fun renderNestedListEntryMember(nestedMemberTargetName: String, nestedMemberTarget: CollectionShape, nestedMember: MemberShape, nestedMemberResolvedName: String, containerName: String, level: Int) { - var nestedContainerName = "${nestedMemberTargetName.removeSurroundingBackticks()}Container$level" - writer.write("var $nestedContainerName = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"${nestedMemberResolvedName}\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - XMLNamespaceTraitGenerator.construct(nestedMember)?.render(writer, nestedContainerName)?.appendKey(xmlNamespaces) - renderListMemberItems(nestedMemberTargetName, nestedMemberTarget, nestedContainerName, level + 1) - } - - private fun renderFlattenedListMemberItems( - memberName: String, - member: MemberShape, - memberTarget: CollectionShape, - containerName: String, - level: Int = 0 - ) { - val nestedMember = memberTarget.member - val nestedMemberTarget = ctx.model.expectShape(memberTarget.member.target) - val nestedMemberTargetName = "${nestedMemberTarget.id.name.toLowerCase()}$level" - val defaultMemberName = if (level == 0) memberName else "member" - val resolvedMemberName = XMLNameTraitGenerator.construct(member, defaultMemberName) - val nestedContainerName = "${memberName.removeSurroundingBackticks()}Container$level" - - writer.openBlock("for $nestedMemberTargetName in $memberName {", "}") { - when (nestedMemberTarget) { - is CollectionShape -> { - val isBoxed = ctx.symbolProvider.toSymbol(memberTarget.member).isBoxed() - if (isBoxed && !(nestedMemberTarget is SetShape)) { - writer.openBlock("if let $nestedMemberTargetName = $nestedMemberTargetName {", "}") { - renderFlattenedListContainer(nestedMemberTargetName, nestedMemberTarget, nestedMember, memberName, member, containerName, level) - } - } else { - renderFlattenedListContainer(nestedMemberTargetName, nestedMemberTarget, nestedMember, memberName, member, containerName, level) - } - } - is MapShape -> { - writer.write("var $nestedContainerName = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"${resolvedMemberName}\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - writer.openBlock("if let $nestedMemberTargetName = $nestedMemberTargetName {", "}") { - renderWrappedMapMemberItem(nestedMemberTargetName, nestedMemberTarget, nestedContainerName, level) - } - } - is TimestampShape -> { - writer.write("var $nestedContainerName = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"$resolvedMemberName\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - XMLNamespaceTraitGenerator.construct(member)?.render(writer, nestedContainerName)?.appendKey(xmlNamespaces) - val codingKey = "Key(\"\")" - TimestampEncodeGenerator( - nestedContainerName, - nestedMemberTargetName, - codingKey, - TimestampHelpers.getTimestampFormat(nestedMember, nestedMemberTarget, defaultTimestampFormat) - ).generate(writer) - } - else -> { - writer.write("var $nestedContainerName = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"$resolvedMemberName\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - XMLNamespaceTraitGenerator.construct(member)?.render(writer, nestedContainerName)?.appendKey(xmlNamespaces) - writer.write("try $nestedContainerName.encode($nestedMemberTargetName, forKey: \$N(\"\"))", ClientRuntimeTypes.Serde.Key) - } + is ListShape -> { + writeListMember(memberShape, targetShape, prefix) } - } - } - private fun renderFlattenedListContainer(nestedMemberTargetName: String, nestedMemberTarget: CollectionShape, nestedMember: MemberShape, memberName: String, member: MemberShape, containerName: String, level: Int) { - var nestedContainerName = "${nestedMemberTargetName.removeSurroundingBackticks()}Container$level" - val defaultMemberName = if (level == 0) memberName else "member" - val memberResolvedName = XMLNameTraitGenerator.construct(member, defaultMemberName) - writer.write("var $nestedContainerName = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"${memberResolvedName}\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - XMLNamespaceTraitGenerator.construct(member)?.render(writer, nestedContainerName)?.appendKey(xmlNamespaces) - renderFlattenedListMemberItems(nestedMemberTargetName, nestedMember, nestedMemberTarget, nestedContainerName, level + 1) - } - - fun renderMapMember(member: MemberShape, memberTarget: MapShape, containerName: String) { - val originalMemberName = member.memberName - val memberName = ctx.symbolProvider.toMemberName(member) - val resolvedMemberName = XMLNameTraitGenerator.construct(member, originalMemberName) - - if (member.hasTrait(XmlFlattenedTrait::class.java)) { - writer.openBlock("if $memberName.isEmpty {", "} else {") { - writer.write("let _ = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"$resolvedMemberName\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) + is MapShape -> { + writeMapMember(memberShape, targetShape, prefix) } - writer.indent() - renderFlattenedMapMemberItem(memberName, member, memberTarget, containerName) - writer.dedent().write("}") - } else { - val nestedContainer = "${resolvedMemberName}Container" - writer.write("var $nestedContainer = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"$resolvedMemberName\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - XMLNamespaceTraitGenerator.construct(member)?.render(writer, nestedContainer)?.appendKey(xmlNamespaces) - renderWrappedMapMemberItem(memberName, memberTarget, nestedContainer) - } - } - - private fun renderWrappedMapMemberItem(memberName: String, mapShape: MapShape, containerName: String, level: Int = 0) { - val keyTargetShape = ctx.model.expectShape(mapShape.key.target) - val valueTargetShape = ctx.model.expectShape(mapShape.value.target) - - val resolvedCodingKeys = Pair( - XMLNameTraitGenerator.construct(mapShape.key, "key"), - XMLNameTraitGenerator.construct(mapShape.value, "value") - ) - - val nestedKeyValueName = Pair("${keyTargetShape.id.name.toLowerCase()}Key$level", "${valueTargetShape.id.name.toLowerCase()}Value$level") - val entryContainerName = "entryContainer$level" - writer.openBlock("for (${nestedKeyValueName.first}, ${nestedKeyValueName.second}) in $memberName {", "}") { - writer.write("var $entryContainerName = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"entry\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - renderMapKey(nestedKeyValueName, resolvedCodingKeys, mapShape, entryContainerName, level) - when (valueTargetShape) { - is MapShape -> { - renderMapNestedValue(nestedKeyValueName, resolvedCodingKeys, mapShape, valueTargetShape, entryContainerName, level) { valueContainer -> - renderWrappedMapMemberItem(nestedKeyValueName.second, valueTargetShape, valueContainer, level + 1) - } - } - is CollectionShape -> { - renderMapNestedValue(nestedKeyValueName, resolvedCodingKeys, mapShape, valueTargetShape, entryContainerName, level) { valueContainer -> - renderListMemberItems(nestedKeyValueName.second, valueTargetShape, valueContainer, level + 1) - } - } - is TimestampShape -> { - renderMapValue(nestedKeyValueName, resolvedCodingKeys, mapShape, entryContainerName, level) { valueContainer -> - val codingKey = "${ClientRuntimeTypes.Serde.Key}(\"\")" - TimestampEncodeGenerator( - valueContainer, - nestedKeyValueName.second, - codingKey, - TimestampHelpers.getTimestampFormat(mapShape.value, valueTargetShape, defaultTimestampFormat) - ).generate(writer) - } - } - else -> { - renderMapValue(nestedKeyValueName, resolvedCodingKeys, mapShape, entryContainerName, level) - } + is TimestampShape -> { + writeTimestampMember(memberShape, targetShape, prefix) } - } - } - - private fun renderFlattenedMapMemberItem(memberName: String, member: MemberShape, mapShape: MapShape, containerName: String, level: Int = 0) { - val keyTargetShape = ctx.model.expectShape(mapShape.key.target) - val valueTargetShape = ctx.model.expectShape(mapShape.value.target) - - val resolvedMemberName = if (level == 0) XMLNameTraitGenerator.construct(member, memberName) else "entry" - - val resolvedCodingKeys = Pair( - XMLNameTraitGenerator.construct(mapShape.key, "key"), - XMLNameTraitGenerator.construct(mapShape.value, "value") - ) - - val nestedKeyValueName = Pair("${keyTargetShape.id.name.toLowerCase()}Key$level", "${valueTargetShape.id.name.toLowerCase()}Value$level") - val nestedContainer = "nestedContainer$level" - writer.openBlock("for (${nestedKeyValueName.first}, ${nestedKeyValueName.second}) in $memberName {", "}") { - writer.write("var $nestedContainer = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"$resolvedMemberName\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - when (valueTargetShape) { - is MapShape -> { - renderMapKey(nestedKeyValueName, resolvedCodingKeys, mapShape, nestedContainer, level) - renderMapNestedValue(nestedKeyValueName, resolvedCodingKeys, mapShape, valueTargetShape, nestedContainer, level) { nestedValueContainer -> - renderFlattenedMapMemberItem(nestedKeyValueName.second, mapShape.value, valueTargetShape, nestedValueContainer, level + 1) - } - } - is CollectionShape -> { - renderMapKey(nestedKeyValueName, resolvedCodingKeys, mapShape, nestedContainer, level) - renderMapNestedValue(nestedKeyValueName, resolvedCodingKeys, mapShape, valueTargetShape, nestedContainer, level) { nestedValueContainer -> - renderListMemberItems(nestedKeyValueName.second, valueTargetShape, nestedValueContainer, level + 1) - } - } - is TimestampShape -> { - renderMapKey(nestedKeyValueName, resolvedCodingKeys, mapShape, nestedContainer, level) - renderMapValue(nestedKeyValueName, resolvedCodingKeys, mapShape, nestedContainer, level) { valueContainer -> - val codingKey = writer.format("\$L(\"\")", ClientRuntimeTypes.Serde.Key) - val code = TimestampEncodeGenerator( - valueContainer, - nestedKeyValueName.second, - codingKey, - TimestampHelpers.getTimestampFormat(mapShape.value, valueTargetShape, defaultTimestampFormat) - ).generate(writer) - } - } - else -> { - if (level == 0) { - val memberNamespaceTraitGenerator = XMLNamespaceTraitGenerator.construct(member) - memberNamespaceTraitGenerator?.render(writer, nestedContainer)?.appendKey(xmlNamespaces) - } - renderMapKey(nestedKeyValueName, resolvedCodingKeys, mapShape, nestedContainer, level) - renderMapValue(nestedKeyValueName, resolvedCodingKeys, mapShape, nestedContainer, level) - } + else -> { + writePropertyMember(memberShape, targetShape, prefix) } } } - private fun renderMapKey( - nestedKeyValueName: Pair, - resolvedCodingKeys: Pair, - mapShape: MapShape, - nestedContainer: String, - level: Int - ) { - val nestedKeyContainer = "keyContainer$level" - writer.write("var $nestedKeyContainer = $nestedContainer.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"${resolvedCodingKeys.first}\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - XMLNamespaceTraitGenerator.construct(mapShape.key)?.render(writer, nestedKeyContainer)?.appendKey(xmlNamespaces) - writer.write("try $nestedKeyContainer.encode(${nestedKeyValueName.first}, forKey: \$N(\"\"))", ClientRuntimeTypes.Serde.Key) + private fun writeStructureOrUnionMember(memberShape: MemberShape, prefix: String) { + val memberName = ctx.symbolProvider.toMemberName(memberShape) + val propertyKey = nodeInfoUtils.nodeInfo(memberShape) + val writingClosure = writingClosureUtils.writingClosure(memberShape) + writer.write( + "try writer[\$L].write(\$L\$L, writingClosure: \$L)", + propertyKey, + prefix, + memberName, + writingClosure + ) } - private fun renderMapValue( - nestedKeyValueName: Pair, - resolvedCodingKeys: Pair, - mapShape: MapShape, - entryContainerName: String, - level: Int, - customValueRenderer: ((String) -> Unit)? = null - ) { - val valueContainerName = "valueContainer$level" - writer.write("var $valueContainerName = $entryContainerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"${resolvedCodingKeys.second}\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - XMLNamespaceTraitGenerator.construct(mapShape.value)?.render(writer, valueContainerName)?.appendKey(xmlNamespaces) - if (customValueRenderer != null) { - customValueRenderer(valueContainerName) - } else { - writer.write("try $valueContainerName.encode(${nestedKeyValueName.second}, forKey: \$N(\"\"))", ClientRuntimeTypes.Serde.Key) - } + private fun writeTimestampMember(memberShape: MemberShape, timestampShape: TimestampShape, prefix: String) { + val memberName = ctx.symbolProvider.toMemberName(memberShape) + val timestampKey = nodeInfoUtils.nodeInfo(memberShape) + val memberTimestampFormatTrait = memberShape.getTrait() + val swiftTimestampFormatCase = TimestampUtils.timestampFormat(memberTimestampFormatTrait, timestampShape) + writer.write( + "try writer[\$L].writeTimestamp(\$L\$L, format: \$L)", + timestampKey, + prefix, + memberName, + swiftTimestampFormatCase + ) } - private fun renderMapNestedValue( - nestedKeyValueName: Pair, - resolvedCodingKeys: Pair, - mapShape: MapShape, - valueTargetShape: Shape, - entryContainerName: String, - level: Int, - nextRenderer: (String) -> Unit - ) { - val nextContainer = "valueContainer${level + 1}" - writer.write("var $nextContainer = $entryContainerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"${resolvedCodingKeys.second}\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - XMLNamespaceTraitGenerator.construct(mapShape.value)?.render(writer, nextContainer)?.appendKey(xmlNamespaces) - nextRenderer(nextContainer) + private fun writePropertyMember(memberShape: MemberShape, targetShape: Shape, prefix: String) { + val memberName = ctx.symbolProvider.toMemberName(memberShape) + val propertyNodeInfo = nodeInfoUtils.nodeInfo(memberShape) + writer.write( + "try writer[\$L].write(\$L\$L)", + propertyNodeInfo, + prefix, + memberName + ) } - fun renderTimestampMember(member: MemberShape, memberTarget: TimestampShape, containerName: String) { + private fun writeListMember(member: MemberShape, listShape: ListShape, prefix: String) { val memberName = ctx.symbolProvider.toMemberName(member) - val originalMemberName = member.memberName - val resolvedMemberName = XMLNameTraitGenerator.construct(member, originalMemberName) - val codingKey = writer.format("\$L(\"\$L\")", ClientRuntimeTypes.Serde.Key, resolvedMemberName) - TimestampEncodeGenerator( - containerName, + val listMemberWriter = writingClosureUtils.writingClosure(listShape.member) + val listKey = nodeInfoUtils.nodeInfo(member) + val isFlattened = member.hasTrait() + val memberNodeInfo = nodeInfoUtils.nodeInfo(listShape.member) + writer.write( + "try writer[\$L].writeList(\$L\$L, memberWritingClosure: \$L, memberNodeInfo: \$L, isFlattened: \$L)", + listKey, + prefix, memberName, - codingKey, - TimestampHelpers.getTimestampFormat(member, memberTarget, defaultTimestampFormat) - ).generate(writer) + listMemberWriter, + memberNodeInfo, + isFlattened + ) } - fun renderScalarMember(member: MemberShape, memberTarget: Shape, containerName: String) { - val symbol = ctx.symbolProvider.toSymbol(member) - val originalMemberName = member.memberName + private fun writeMapMember(member: MemberShape, mapShape: MapShape, prefix: String) { val memberName = ctx.symbolProvider.toMemberName(member) - val resolvedMemberName = XMLNameTraitGenerator.construct(member, originalMemberName).toString() - val isBoxed = symbol.isBoxed() - if (isBoxed) { - writer.openBlock("if let $memberName = $memberName {", "}") { - val namespaceTraitGenerator = XMLNamespaceTraitGenerator.construct(member) - val nestedContainerName = "${memberName.removeSurroundingBackticks()}Container" - renderItem(writer, namespaceTraitGenerator, nestedContainerName, containerName, memberName, memberTarget, resolvedMemberName) - } - } else { - if (MemberShapeEncodeConstants.primitiveSymbols.contains(memberTarget.type)) { - val defaultValue = getDefaultValueOfShapeType(memberTarget.type) - writer.openBlock("if $memberName != $defaultValue {", "}") { - writer.write("try $containerName.encode($memberName, forKey: \$N(\"$resolvedMemberName\"))", ClientRuntimeTypes.Serde.Key) - } - } else { - writer.write("try $containerName.encode($memberName, forKey: \$N(\"$resolvedMemberName\"))", ClientRuntimeTypes.Serde.Key) - } - } - } - fun renderEncodeAssociatedType(member: MemberShape, memberTarget: Shape, containerName: String) { - val memberName = ctx.symbolProvider.toMemberName(member) - val originalMemberName = member.memberName - val namespaceTraitGenerator = XMLNamespaceTraitGenerator.construct(member) - val resolvedMemberName = XMLNameTraitGenerator.construct(member, originalMemberName).toString() - val nestedContainerName = "${memberName.removeSurroundingBackticks()}Container" - renderItem(writer, namespaceTraitGenerator, nestedContainerName, containerName, memberName, memberTarget, resolvedMemberName) + val mapKey = nodeInfoUtils.nodeInfo(member) + val keyNodeInfo = nodeInfoUtils.nodeInfo(mapShape.key) + val valueNodeInfo = nodeInfoUtils.nodeInfo(mapShape.value) + val valueWriter = writingClosureUtils.writingClosure(mapShape.value) + val isFlattened = member.hasTrait() + writer.write( + "try writer[\$L].writeMap(\$L\$L, valueWritingClosure: \$L, keyNodeInfo: \$L, valueNodeInfo: \$L, isFlattened: \$L)", + mapKey, + prefix, + memberName, + valueWriter, + keyNodeInfo, + valueNodeInfo, + isFlattened + ) } +} + +object TimestampUtils { - private fun renderItem(writer: SwiftWriter, XMLNamespaceTraitGenerator: XMLNamespaceTraitGenerator?, nestedContainerName: String, containerName: String, memberName: String, memberTarget: Shape, resolvedMemberName: String) { - XMLNamespaceTraitGenerator?.let { - writer.write("var $nestedContainerName = $containerName.nestedContainer(keyedBy: \$N.self, forKey: \$N(\"${resolvedMemberName}\"))", ClientRuntimeTypes.Serde.Key, ClientRuntimeTypes.Serde.Key) - writer.write("try $nestedContainerName.encode($memberName, forKey: \$N(\"\"))", ClientRuntimeTypes.Serde.Key) - it.render(writer, nestedContainerName) - it.appendKey(xmlNamespaces) - } ?: run { - writer.write("try $containerName.encode($memberName, forKey: \$N(\"${resolvedMemberName}\"))", ClientRuntimeTypes.Serde.Key) + fun timestampFormat(memberTimestampFormatTrait: TimestampFormatTrait?, timestampShape: TimestampShape): String { + val timestampFormatTrait = memberTimestampFormatTrait ?: timestampShape.getTrait() ?: TimestampFormatTrait(TimestampFormatTrait.DATE_TIME) + return when (timestampFormatTrait.value) { + TimestampFormatTrait.EPOCH_SECONDS -> ".epochSeconds" + TimestampFormatTrait.HTTP_DATE -> ".httpDate" + else -> ".dateTime" } } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/NodeInfoUtils.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/NodeInfoUtils.kt new file mode 100644 index 000000000..2fcd6d3a7 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/NodeInfoUtils.kt @@ -0,0 +1,65 @@ +package software.amazon.smithy.swift.codegen.integration.serde.xml + +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.traits.XmlAttributeTrait +import software.amazon.smithy.model.traits.XmlNameTrait +import software.amazon.smithy.model.traits.XmlNamespaceTrait +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.model.getTrait +import software.amazon.smithy.swift.codegen.model.hasTrait + +class NodeInfoUtils( + val ctx: ProtocolGenerator.GenerationContext, + val writer: SwiftWriter +) { + + fun nodeInfo(shape: Shape): String { + val xmlName = shape.getTrait()?.value + val symbol = ctx.symbolProvider.toSymbol(shape) + val resolvedName = xmlName ?: symbol.name + + val xmlNamespaceTrait = shape.getTrait() ?: ctx.service.getTrait() + val xmlNamespaceParam = namespaceParam(xmlNamespaceTrait) + + return writer.format( + ".init(\$S\$L)", + resolvedName, + xmlNamespaceParam + ) + } + + fun nodeInfo(member: MemberShape, forRootNode: Boolean = false): String { + val targetShape = ctx.model.expectShape(member.target) + + val resolvedName = if (forRootNode) { + val xmlName = member.getTrait()?.value ?: targetShape.getTrait()?.value + xmlName ?: ctx.symbolProvider.toSymbol(targetShape).name + } else { + member.getTrait()?.value ?: member.memberName + } + + val xmlAttributeParam = ", location: .attribute".takeIf { member.hasTrait() } ?: "" + + val xmlNamespaceTrait = member.getTrait() ?: targetShape.getTrait() ?: ctx.service.getTrait().takeIf { forRootNode } + val xmlNamespaceParam = namespaceParam(xmlNamespaceTrait) + + return writer.format( + ".init(\$S\$L\$L)", + resolvedName, + xmlAttributeParam, + xmlNamespaceParam + ) + } + + private fun namespaceParam(xmlNamespaceTrait: XmlNamespaceTrait?): String { + return xmlNamespaceTrait?.let { + writer.format( + ", namespace: .init(prefix: \$S, uri: \$S)", + it.prefix, + it.uri + ) + } ?: "" + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/StructEncodeXMLGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/StructEncodeXMLGenerator.kt index 64c12e9dd..66b6f96e5 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/StructEncodeXMLGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/StructEncodeXMLGenerator.kt @@ -5,91 +5,33 @@ package software.amazon.smithy.swift.codegen.integration.serde.json -import software.amazon.smithy.model.shapes.CollectionShape -import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.MemberShape import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.shapes.TimestampShape -import software.amazon.smithy.model.traits.TimestampFormatTrait -import software.amazon.smithy.swift.codegen.ClientRuntimeTypes -import software.amazon.smithy.swift.codegen.SwiftTypes +import software.amazon.smithy.swift.codegen.SmithyXMLTypes +import software.amazon.smithy.swift.codegen.SwiftDependency import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator -import software.amazon.smithy.swift.codegen.integration.serde.xml.trait.XMLNamespaceTraitGenerator -import software.amazon.smithy.swift.codegen.model.isBoxed class StructEncodeXMLGenerator( private val ctx: ProtocolGenerator.GenerationContext, private val shapeContainingMembers: Shape, private val members: List, - private val writer: SwiftWriter, - private val defaultTimestampFormat: TimestampFormatTrait.Format -) : MemberShapeEncodeXMLGenerator(ctx, writer, defaultTimestampFormat) { + private val writer: SwiftWriter +) : MemberShapeEncodeXMLGenerator(ctx, writer) { override fun render() { - writer.openBlock("public func encode(to encoder: \$N) throws {", "}", SwiftTypes.Encoder) { - if (members.isNotEmpty()) { - renderEncodeBody() - } - } - } - - private fun renderEncodeBody() { - val containerName = "container" - writer.write("var $containerName = encoder.container(keyedBy: \$N.self)", ClientRuntimeTypes.Serde.Key) - renderTopLevelNamespace(containerName) - - val membersSortedByName: List = members.sortedBy { it.memberName } - membersSortedByName.forEach { member -> - renderSingleMember(member, containerName) - } - } - private fun renderTopLevelNamespace(containerName: String) { - val serviceNamespace = XMLNamespaceTraitGenerator.construct(ctx.service) - val shapeContainingMembersNamespace = XMLNamespaceTraitGenerator.construct(shapeContainingMembers) - val namespace = if (serviceNamespace != null && shapeContainingMembersNamespace == null) { - serviceNamespace - } else { - shapeContainingMembersNamespace - } - - namespace?.let { - writer.openBlock("if encoder.codingPath.isEmpty {", "}") { - it.render(writer, containerName) - it.appendKey(xmlNamespaces) - } - } - } - - private fun renderSingleMember(member: MemberShape, containerName: String) { - val memberTarget = ctx.model.expectShape(member.target) - val memberName = ctx.symbolProvider.toMemberName(member) - - when (memberTarget) { - is CollectionShape -> { - writer.openBlock("if let $memberName = $memberName {", "}") { - renderListMember(member, memberTarget, containerName) - } - } - is MapShape -> { - writer.openBlock("if let $memberName = $memberName {", "}") { - renderMapMember(member, memberTarget, containerName) - } - } - is TimestampShape -> { - val symbol = ctx.symbolProvider.toSymbol(member) - val isBoxed = symbol.isBoxed() - if (isBoxed) { - writer.openBlock("if let $memberName = $memberName {", "}") { - renderTimestampMember(member, memberTarget, containerName) - } - } else { - renderTimestampMember(member, memberTarget, containerName) - } - } - else -> { - renderScalarMember(member, memberTarget, containerName) - } + writer.addImport(SwiftDependency.SMITHY_XML.target) + val structSymbol = ctx.symbolProvider.toSymbol(shapeContainingMembers) + writer.openBlock( + "static func writingClosure(_ value: \$N?, to writer: \$N) throws {", "}", + structSymbol, + SmithyXMLTypes.Writer + ) { + writer.write( + "guard \$L else { writer.detach(); return }", + "value != nil".takeIf { members.isEmpty() } ?: "let value" + ) + members.sortedBy { it.memberName }.forEach { writeMember(it, false) } } } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/UnionDecodeXMLGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/UnionDecodeXMLGenerator.kt index d0e422b32..94ea29288 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/UnionDecodeXMLGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/UnionDecodeXMLGenerator.kt @@ -13,11 +13,9 @@ import software.amazon.smithy.model.shapes.TimestampShape import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.swift.codegen.SwiftTypes import software.amazon.smithy.swift.codegen.SwiftWriter -import software.amazon.smithy.swift.codegen.customtraits.SwiftBoxTrait import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.serde.TimestampDecodeGenerator import software.amazon.smithy.swift.codegen.integration.serde.TimestampHelpers -import software.amazon.smithy.swift.codegen.model.recursiveSymbol import software.amazon.smithy.swift.codegen.removeSurroundingBackticks class UnionDecodeXMLGenerator( @@ -93,9 +91,6 @@ class UnionDecodeXMLGenerator( val memberName = ctx.symbolProvider.toMemberName(member) val memberNameUnquoted = memberName.removeSurrounding("`", "`") var memberTargetSymbol = ctx.symbolProvider.toSymbol(memberTarget) - if (member.hasTrait(SwiftBoxTrait::class.java)) { - memberTargetSymbol = memberTargetSymbol.recursiveSymbol() - } val decodedMemberName = "${memberName}Decoded" writer.write("let $decodedMemberName = try $containerName.decode(\$N.self, forKey: .$memberNameUnquoted)", memberTargetSymbol) renderAssigningDecodedMember(memberName, decodedMemberName) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/UnionEncodeXMLGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/UnionEncodeXMLGenerator.kt index 9fe3a7419..1d9957718 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/UnionEncodeXMLGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/UnionEncodeXMLGenerator.kt @@ -5,53 +5,41 @@ package software.amazon.smithy.swift.codegen.integration.serde.xml -import software.amazon.smithy.model.shapes.CollectionShape -import software.amazon.smithy.model.shapes.MapShape import software.amazon.smithy.model.shapes.MemberShape -import software.amazon.smithy.model.shapes.TimestampShape -import software.amazon.smithy.model.traits.TimestampFormatTrait -import software.amazon.smithy.swift.codegen.ClientRuntimeTypes -import software.amazon.smithy.swift.codegen.SwiftTypes +import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.swift.codegen.SmithyXMLTypes +import software.amazon.smithy.swift.codegen.SwiftDependency import software.amazon.smithy.swift.codegen.SwiftWriter import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.serde.json.MemberShapeEncodeXMLGenerator class UnionEncodeXMLGenerator( private val ctx: ProtocolGenerator.GenerationContext, + private val shapeContainingMembers: Shape, private val members: List, - private val writer: SwiftWriter, - defaultTimestampFormat: TimestampFormatTrait.Format -) : MemberShapeEncodeXMLGenerator(ctx, writer, defaultTimestampFormat) { + private val writer: SwiftWriter +) : MemberShapeEncodeXMLGenerator(ctx, writer) { override fun render() { - val containerName = "container" - writer.openBlock("public func encode(to encoder: \$N) throws {", "}", SwiftTypes.Encoder) { - writer.write("var $containerName = encoder.container(keyedBy: \$N.self)", ClientRuntimeTypes.Serde.Key) - writer.openBlock("switch self {", "}") { + writer.addImport(SwiftDependency.SMITHY_XML.target) + val structSymbol = ctx.symbolProvider.toSymbol(shapeContainingMembers) + writer.openBlock( + "static func writingClosure(_ value: \$N?, to writer: \$N) throws {", "}", + structSymbol, + SmithyXMLTypes.Writer + ) { + writer.write("guard let value else { writer.detach(); return }") + writer.openBlock("switch value {", "}") { val membersSortedByName: List = members.sortedBy { it.memberName } membersSortedByName.forEach { member -> - val memberTarget = ctx.model.expectShape(member.target) val memberName = ctx.symbolProvider.toMemberName(member) writer.write("case let .\$L(\$L):", memberName, memberName) writer.indent() - when (memberTarget) { - is CollectionShape -> { - renderListMember(member, memberTarget, containerName) - } - is MapShape -> { - renderMapMember(member, memberTarget, containerName) - } - is TimestampShape -> { - renderTimestampMember(member, memberTarget, containerName) - } - else -> { - renderEncodeAssociatedType(member, memberTarget, containerName) - } - } + writeMember(member, true) writer.dedent() } writer.write("case let .sdkUnknown(sdkUnknown):") writer.indent() - writer.write("try container.encode(sdkUnknown, forKey: \$N(\"sdkUnknown\"))", ClientRuntimeTypes.Serde.Key) + writer.write("try writer[.init(\"sdkUnknown\")].write(sdkUnknown)") writer.dedent() } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/trait/XMLNameTraitGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/trait/XMLNameTraitGenerator.kt index e527e1bc5..4167edd8a 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/trait/XMLNameTraitGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/trait/XMLNameTraitGenerator.kt @@ -8,6 +8,7 @@ package software.amazon.smithy.swift.codegen.integration.serde.xml.trait import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.traits.XmlNameTrait import software.amazon.smithy.swift.codegen.model.getTrait +import software.amazon.smithy.swift.codegen.removeSurroundingBackticks class XMLNameTraitGenerator(val xmlNameValue: String) { companion object { @@ -15,7 +16,7 @@ class XMLNameTraitGenerator(val xmlNameValue: String) { shape.getTrait()?.let { return XMLNameTraitGenerator(it.value.toString()) } - val unquotedDefaultMemberName = defaultMemberName.removeSurrounding("`", "`") + val unquotedDefaultMemberName = defaultMemberName.removeSurroundingBackticks() return XMLNameTraitGenerator(unquotedDefaultMemberName) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/trait/XMLNamespaceTraitGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/trait/XMLNamespaceTraitGenerator.kt deleted file mode 100644 index 585aea257..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/trait/XMLNamespaceTraitGenerator.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package software.amazon.smithy.swift.codegen.integration.serde.xml.trait - -import software.amazon.smithy.model.shapes.Shape -import software.amazon.smithy.model.traits.XmlNamespaceTrait -import software.amazon.smithy.swift.codegen.ClientRuntimeTypes -import software.amazon.smithy.swift.codegen.SwiftWriter - -class XMLNamespaceTraitGenerator(val key: String, val value: String) { - companion object { - fun construct(shape: Shape): XMLNamespaceTraitGenerator? { - if (shape.hasTrait(XmlNamespaceTrait::class.java)) { - val trait = shape.getTrait(XmlNamespaceTrait::class.java).get() - val key = if (trait.prefix.isPresent) "xmlns:${trait.prefix.get()}" else "xmlns" - val namespaceValue = trait.uri - return XMLNamespaceTraitGenerator(key, namespaceValue) - } - return null - } - } - - fun render(writer: SwiftWriter, container: String): XMLNamespaceTraitGenerator { - writer.write("try $container.encode(\"$value\", forKey: \$N(\"${key}\"))", ClientRuntimeTypes.Serde.Key) - return this - } - - fun appendKey(xmlNamespaces: MutableSet): XMLNamespaceTraitGenerator { - xmlNamespaces.add(key) - return this - } -} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt index c81302a98..caed48741 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt @@ -2,6 +2,7 @@ package software.amazon.smithy.swift.codegen.middleware import software.amazon.smithy.model.Model import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.ClientRuntimeTypes.Middleware.OperationStack import software.amazon.smithy.swift.codegen.SwiftWriter @@ -26,7 +27,7 @@ class MiddlewareExecutionGenerator( private val model: Model = ctx.model private val symbolProvider = ctx.symbolProvider - fun render(op: OperationShape, onError: (SwiftWriter, String) -> Unit) { + fun render(service: ServiceShape, op: OperationShape, onError: (SwiftWriter, String) -> Unit) { val operationErrorName = "${op.toUpperCamelCase()}OutputError" val inputShapeName = MiddlewareShapeUtils.inputSymbol(symbolProvider, ctx.model, op).name val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, ctx.model, op).name @@ -36,7 +37,7 @@ class MiddlewareExecutionGenerator( } httpProtocolCustomizable.renderEventStreamAttributes(ctx, writer, op) writer.write("var $operationStackName = \$N<$inputShapeName, $outputShapeName, $operationErrorName>(id: \"${op.toLowerCamelCase()}\")", OperationStack) - renderMiddlewares(op, operationStackName) + renderMiddlewares(ctx, op, operationStackName) } private fun renderContextAttributes(op: OperationShape) { @@ -66,11 +67,11 @@ class MiddlewareExecutionGenerator( } } - private fun renderMiddlewares(op: OperationShape, operationStackName: String) { - operationMiddleware.renderMiddleware(writer, op, operationStackName, MiddlewareStep.INITIALIZESTEP) - operationMiddleware.renderMiddleware(writer, op, operationStackName, MiddlewareStep.BUILDSTEP) - operationMiddleware.renderMiddleware(writer, op, operationStackName, MiddlewareStep.SERIALIZESTEP) - operationMiddleware.renderMiddleware(writer, op, operationStackName, MiddlewareStep.FINALIZESTEP) - operationMiddleware.renderMiddleware(writer, op, operationStackName, MiddlewareStep.DESERIALIZESTEP) + private fun renderMiddlewares(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, operationStackName: String) { + operationMiddleware.renderMiddleware(ctx, writer, op, operationStackName, MiddlewareStep.INITIALIZESTEP) + operationMiddleware.renderMiddleware(ctx, writer, op, operationStackName, MiddlewareStep.BUILDSTEP) + operationMiddleware.renderMiddleware(ctx, writer, op, operationStackName, MiddlewareStep.SERIALIZESTEP) + operationMiddleware.renderMiddleware(ctx, writer, op, operationStackName, MiddlewareStep.FINALIZESTEP) + operationMiddleware.renderMiddleware(ctx, writer, op, operationStackName, MiddlewareStep.DESERIALIZESTEP) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareRenderable.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareRenderable.kt index cf26c4c53..631ea2166 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareRenderable.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareRenderable.kt @@ -7,6 +7,7 @@ package software.amazon.smithy.swift.codegen.middleware import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator /** * Interface that allows middleware to be registered and configured with the generated protocol client @@ -20,5 +21,5 @@ interface MiddlewareRenderable { val position: MiddlewarePosition - fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) + fun render(ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/OperationMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/OperationMiddleware.kt index 7b17d0882..08bfe2007 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/OperationMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/OperationMiddleware.kt @@ -2,6 +2,7 @@ package software.amazon.smithy.swift.codegen.middleware import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator interface OperationMiddleware { fun appendMiddleware(operation: OperationShape, renderableMiddleware: MiddlewareRenderable) @@ -12,6 +13,7 @@ interface OperationMiddleware { fun clone(): OperationMiddleware fun renderMiddleware( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, operation: OperationShape, operationStackName: String, diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/OperationMiddlewareGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/OperationMiddlewareGenerator.kt index 1f4888251..716e4acba 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/OperationMiddlewareGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/OperationMiddlewareGenerator.kt @@ -2,6 +2,7 @@ package software.amazon.smithy.swift.codegen.middleware import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator open class OperationMiddlewareGenerator(val mutableHashMap: MutableMap = mutableMapOf()) : OperationMiddleware { @@ -46,6 +47,7 @@ open class OperationMiddlewareGenerator(val mutableHashMap: MutableMap - */ -fun Symbol.recursiveSymbol(): Symbol { - return Symbol.builder() - .addDependency(SwiftDependency.CLIENT_RUNTIME) - .name("Box<$fullName>") - .putProperty(SymbolProperty.BOXED_KEY, isBoxed()) - .putProperty(SymbolProperty.DEFAULT_VALUE_KEY, defaultValue()) - .build() -} - /** * Gets the default value for the symbol if present, else null */ diff --git a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt index a3c58ee7f..10b97b3ce 100644 --- a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt @@ -7,16 +7,9 @@ class ContentMd5MiddlewareTests { val context = setupTests("Isolated/contentmd5checksum.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/RestXmlProtocolClient.swift") val expectedContents = """ -extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { - /// Performs the `IdempotencyTokenWithStructure` operation on the `RestXml` service. - /// - /// This is a very cool operation. - /// - /// - Parameter IdempotencyTokenWithStructureInput : [no documentation found] - /// - /// - Returns: `IdempotencyTokenWithStructureOutput` : [no documentation found] public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput { + let encoder = self.encoder let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -33,7 +26,7 @@ extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.ContentMD5Middleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml")) - operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "IdempotencyToken")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: SmithyXML.XMLReadWrite.documentWritingClosure(rootNodeInfo: .init("IdempotencyToken")), inputWritingClosure: IdempotencyTokenWithStructureInput.writingClosure(_:to:))) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) @@ -41,8 +34,6 @@ extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) return result } - -} """ contents.shouldContainOnlyOnce(expectedContents) } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpBodyMiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpBodyMiddlewareTests.kt deleted file mode 100644 index 03a4ab8ae..000000000 --- a/smithy-swift-codegen/src/test/kotlin/HttpBodyMiddlewareTests.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Test -import software.amazon.smithy.swift.codegen.model.AddOperationShapes - -class HttpBodyMiddlewareTests { - private var model = javaClass.getResource("http-binding-protocol-generator-test.smithy").asSmithy() - var newTestContext: TestContext - init { - newTestContext = newTestContext() - newTestContext.generator.generateSerializers(newTestContext.generationCtx) - newTestContext.generator.generateProtocolClient(newTestContext.generationCtx) - newTestContext.generator.generateDeserializers(newTestContext.generationCtx) - newTestContext.generator.generateCodableConformanceForNestedTypes(newTestContext.generationCtx) - newTestContext.generationCtx.delegator.flushWriters() - } - private fun newTestContext(): TestContext { - val settings = model.defaultSettings() - model = AddOperationShapes.execute(model, settings.getService(model), settings.moduleName) - return model.newTestContext() - } - - @Test - fun `it builds body middleware for explicit string payloads`() { - val contents = getModelFileContents("example", "ExplicitStringInput+BodyMiddleware.swift", newTestContext.manifest) - contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - public struct ExplicitStringInputBodyMiddleware: ClientRuntime.Middleware { - public let id: Swift.String = "ExplicitStringInputBodyMiddleware" - - public init() {} - - public func handle(context: Context, - input: ClientRuntime.SerializeStepInput, - next: H) async throws -> ClientRuntime.OperationOutput - where H: Handler, - Self.MInput == H.Input, - Self.MOutput == H.Output, - Self.Context == H.Context - { - if let payload1 = input.operationInput.payload1 { - let payload1Data = payload1.data(using: .utf8) - let payload1Body = ClientRuntime.HttpBody.data(payload1Data) - input.builder.withBody(payload1Body) - } - return try await next.handle(context: context, input: input) - } - - public typealias MInput = ClientRuntime.SerializeStepInput - public typealias MOutput = ClientRuntime.OperationOutput - public typealias Context = ClientRuntime.HttpContext - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `it builds body middleware for explicit blob payloads`() { - val contents = getModelFileContents("example", "ExplicitBlobInput+BodyMiddleware.swift", newTestContext.manifest) - contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - public struct ExplicitBlobInputBodyMiddleware: ClientRuntime.Middleware { - public let id: Swift.String = "ExplicitBlobInputBodyMiddleware" - - public init() {} - - public func handle(context: Context, - input: ClientRuntime.SerializeStepInput, - next: H) async throws -> ClientRuntime.OperationOutput - where H: Handler, - Self.MInput == H.Input, - Self.MOutput == H.Output, - Self.Context == H.Context - { - if let payload1 = input.operationInput.payload1 { - let payload1Data = payload1 - let payload1Body = ClientRuntime.HttpBody.data(payload1Data) - input.builder.withBody(payload1Body) - } - return try await next.handle(context: context, input: input) - } - - public typealias MInput = ClientRuntime.SerializeStepInput - public typealias MOutput = ClientRuntime.OperationOutput - public typealias Context = ClientRuntime.HttpContext - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `it builds body middleware for explicit streaming blob payloads`() { - val contents = getModelFileContents("example", "ExplicitBlobStreamInput+BodyMiddleware.swift", newTestContext.manifest) - contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - public struct ExplicitBlobStreamInputBodyMiddleware: ClientRuntime.Middleware { - public let id: Swift.String = "ExplicitBlobStreamInputBodyMiddleware" - - public init() {} - - public func handle(context: Context, - input: ClientRuntime.SerializeStepInput, - next: H) async throws -> ClientRuntime.OperationOutput - where H: Handler, - Self.MInput == H.Input, - Self.MOutput == H.Output, - Self.Context == H.Context - { - if let payload1 = input.operationInput.payload1 { - let payload1Body = ClientRuntime.HttpBody(byteStream: payload1) - input.builder.withBody(payload1Body) - } - return try await next.handle(context: context, input: input) - } - - public typealias MInput = ClientRuntime.SerializeStepInput - public typealias MOutput = ClientRuntime.OperationOutput - public typealias Context = ClientRuntime.HttpContext - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `it builds body middleware for explicit struct payloads`() { - val contents = getModelFileContents("example", "ExplicitStructInput+BodyMiddleware.swift", newTestContext.manifest) - contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - public struct ExplicitStructInputBodyMiddleware: ClientRuntime.Middleware { - public let id: Swift.String = "ExplicitStructInputBodyMiddleware" - - public init() {} - - public func handle(context: Context, - input: ClientRuntime.SerializeStepInput, - next: H) async throws -> ClientRuntime.OperationOutput - where H: Handler, - Self.MInput == H.Input, - Self.MOutput == H.Output, - Self.Context == H.Context - { - do { - let encoder = context.getEncoder() - if let payload1 = input.operationInput.payload1 { - let payload1Data = try encoder.encode(payload1) - let payload1Body = ClientRuntime.HttpBody.data(payload1Data) - input.builder.withBody(payload1Body) - } else { - if encoder is JSONEncoder { - // Encode an empty body as an empty structure in JSON - let payload1Data = "{}".data(using: .utf8)! - let payload1Body = ClientRuntime.HttpBody.data(payload1Data) - input.builder.withBody(payload1Body) - } - } - } catch let err { - throw ClientRuntime.ClientError.unknownError(err.localizedDescription) - } - return try await next.handle(context: context, input: input) - } - - public typealias MInput = ClientRuntime.SerializeStepInput - public typealias MOutput = ClientRuntime.OperationOutput - public typealias Context = ClientRuntime.HttpContext - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index 5324726f4..89ec7674f 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -106,16 +106,9 @@ class HttpProtocolClientGeneratorTests { val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() val expected = """ -extension RestJsonProtocolClient: RestJsonProtocolClientProtocol { - /// Performs the `AllocateWidget` operation on the `Example` service. - /// - /// This is a very cool operation. - /// - /// - Parameter AllocateWidgetInput : [no documentation found] - /// - /// - Returns: `AllocateWidgetOutput` : [no documentation found] public func allocateWidget(input: AllocateWidgetInput) async throws -> AllocateWidgetOutput { + let encoder = self.encoder let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -131,7 +124,7 @@ extension RestJsonProtocolClient: RestJsonProtocolClientProtocol { operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "AllocateWidgetInput")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) @@ -150,31 +143,32 @@ extension RestJsonProtocolClient: RestJsonProtocolClientProtocol { contents.shouldSyntacticSanityCheck() val expected = """ - public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput - { - let context = ClientRuntime.HttpContextBuilder() - .withEncoder(value: encoder) - .withDecoder(value: decoder) - .withMethod(value: .post) - .withServiceName(value: serviceName) - .withOperation(value: "unsignedFooBlobStream") - .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) - .withLogger(value: config.logger) - .withPartitionID(value: config.partitionID) - .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "GetFooStreamingRequest")) - operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: false, unsignedPayload: true)) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) - let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) - return result - } - """.trimIndent() + public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput + { + let encoder = self.encoder + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStream") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: false, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } +""" contents.shouldContainOnlyOnce(expected) } @@ -183,33 +177,33 @@ extension RestJsonProtocolClient: RestJsonProtocolClientProtocol { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() - val expected = - """ - public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput - { - let context = ClientRuntime.HttpContextBuilder() - .withEncoder(value: encoder) - .withDecoder(value: decoder) - .withMethod(value: .post) - .withServiceName(value: serviceName) - .withOperation(value: "unsignedFooBlobStreamWithLength") - .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) - .withLogger(value: config.logger) - .withPartitionID(value: config.partitionID) - .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) - operation.serializeStep.intercept(position: .after, middleware: UnsignedFooBlobStreamWithLengthInputBodyMiddleware()) - operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) - let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) - return result - } - """.trimIndent() + val expected = """ + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput + { + let encoder = self.encoder + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BlobStreamBodyMiddleware(keyPath: \.payload1)) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } +""" contents.shouldContainOnlyOnce(expected) } @@ -218,33 +212,33 @@ extension RestJsonProtocolClient: RestJsonProtocolClientProtocol { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() - val expected = - """ - public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput - { - let context = ClientRuntime.HttpContextBuilder() - .withEncoder(value: encoder) - .withDecoder(value: decoder) - .withMethod(value: .post) - .withServiceName(value: serviceName) - .withOperation(value: "unsignedFooBlobStreamWithLength") - .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) - .withLogger(value: config.logger) - .withPartitionID(value: config.partitionID) - .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) - operation.serializeStep.intercept(position: .after, middleware: UnsignedFooBlobStreamWithLengthInputBodyMiddleware()) - operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) - let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) - return result - } - """.trimIndent() + val expected = """ + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput + { + let encoder = self.encoder + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BlobStreamBodyMiddleware(keyPath: \.payload1)) + operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) + return result + } +""" contents.shouldContainOnlyOnce(expected) } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt index b1e69b606..7f4a9c42b 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt @@ -95,7 +95,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware()) operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.QueryItemMiddleware()) operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "SmokeTestRequest")) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware( @@ -103,7 +103,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(SmokeTestInputBody.self, from: expectedData) let actualObj = try decoder.decode(SmokeTestInputBody.self, from: actualData) @@ -181,7 +181,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { return try await next.handle(context: context, input: input) } operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "text/plain")) - operationStack.serializeStep.intercept(position: .after, middleware: ExplicitStringInputBodyMiddleware()) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.StringBodyMiddleware(keyPath: \.payload1)) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware( @@ -189,7 +189,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in XCTAssertEqual(expectedData, actualData) } }) @@ -317,7 +317,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { } operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware()) operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "SimpleScalarPropertiesInputOutput")) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware( @@ -325,7 +325,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(SimpleScalarPropertiesInputBody.self, from: expectedData) let actualObj = try decoder.decode(SimpleScalarPropertiesInputBody.self, from: actualData) @@ -409,7 +409,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { } operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware()) operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) - operationStack.serializeStep.intercept(position: .after, middleware: StreamingTraitsInputBodyMiddleware()) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.BlobStreamBodyMiddleware(keyPath: \.blob)) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware( @@ -417,7 +417,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in XCTAssertEqual(expectedData, actualData) } }) @@ -556,7 +556,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { return try await next.handle(context: context, input: input) } operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "UnionInputOutput")) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware( @@ -564,7 +564,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(JsonUnionsInputBody.self, from: expectedData) let actualObj = try decoder.decode(JsonUnionsInputBody.self, from: actualData) @@ -594,8 +594,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { fun `it creates a unit test for recursive shapes`() { val contents = getTestFileContents("example", "RecursiveShapesRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testRestJsonRecursiveShapes() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -632,16 +631,12 @@ class HttpProtocolUnitTestRequestGeneratorTests { let input = RecursiveShapesInput( nested: RecursiveShapesInputOutputNested1( foo: "Foo1", - nested: Box( - value: RecursiveShapesInputOutputNested2( - bar: "Bar1", - recursiveMember: RecursiveShapesInputOutputNested1( - foo: "Foo2", - nested: Box( - value: RecursiveShapesInputOutputNested2( - bar: "Bar2" - ) - ) + nested: RecursiveShapesInputOutputNested2( + bar: "Bar1", + recursiveMember: RecursiveShapesInputOutputNested1( + foo: "Foo2", + nested: RecursiveShapesInputOutputNested2( + bar: "Bar2" ) ) ) @@ -665,7 +660,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { return try await next.handle(context: context, input: input) } operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "RecursiveShapesInputOutput")) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware( @@ -673,7 +668,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(RecursiveShapesInputBody.self, from: expectedData) let actualObj = try decoder.decode(RecursiveShapesInputBody.self, from: actualData) @@ -758,7 +753,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { return try await next.handle(context: context, input: input) } operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "InlineDocumentInputOutput")) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware( @@ -766,7 +761,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(InlineDocumentInputBody.self, from: expectedData) let actualObj = try decoder.decode(InlineDocumentInputBody.self, from: actualData) @@ -797,8 +792,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { fun `it creates a unit test for inline document as payload`() { val contents = getTestFileContents("example", "InlineDocumentAsPayloadRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testInlineDocumentAsPayloadInput() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -848,7 +842,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { return try await next.handle(context: context, input: input) } operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) - operationStack.serializeStep.intercept(position: .after, middleware: InlineDocumentAsPayloadInputBodyMiddleware()) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.PayloadBodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure(), keyPath: \.documentValue, defaultBody: "{}")) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware( @@ -856,7 +850,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(ClientRuntime.Document.self, from: expectedData) let actualObj = try decoder.decode(ClientRuntime.Document.self, from: actualData) @@ -878,7 +872,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { throw serviceError }) } - """ + } +""" contents.shouldContainOnlyOnce(expectedContents) } } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestResponseGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestResponseGeneratorTests.kt index c6401fe37..7b6a590ec 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestResponseGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestResponseGeneratorTests.kt @@ -235,16 +235,12 @@ open class HttpProtocolUnitTestResponseGeneratorTests { let expected = RecursiveShapesOutput( nested: RecursiveShapesInputOutputNested1( foo: "Foo1", - nested: Box( - value: RecursiveShapesInputOutputNested2( - bar: "Bar1", - recursiveMember: RecursiveShapesInputOutputNested1( - foo: "Foo2", - nested: Box( - value: RecursiveShapesInputOutputNested2( - bar: "Bar2" - ) - ) + nested: RecursiveShapesInputOutputNested2( + bar: "Bar1", + recursiveMember: RecursiveShapesInputOutputNested1( + foo: "Foo2", + nested: RecursiveShapesInputOutputNested2( + bar: "Bar2" ) ) ) @@ -254,6 +250,7 @@ open class HttpProtocolUnitTestResponseGeneratorTests { XCTAssertEqual(expected.nested, actual.nested) } +} """ contents.shouldContainOnlyOnce(expectedContents) } diff --git a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt index e514406fe..f99f6e185 100644 --- a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt @@ -7,16 +7,9 @@ class IdempotencyTokenTraitTests { val context = setupTests("Isolated/idempotencyToken.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/RestXmlProtocolClient.swift") val expectedContents = """ -extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { - /// Performs the `IdempotencyTokenWithStructure` operation on the `RestXml` service. - /// - /// This is a very cool operation. - /// - /// - Parameter IdempotencyTokenWithStructureInput : [no documentation found] - /// - /// - Returns: `IdempotencyTokenWithStructureOutput` : [no documentation found] public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput { + let encoder = self.encoder let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -32,7 +25,7 @@ extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml")) - operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.SerializableBodyMiddleware(xmlName: "IdempotencyToken")) + operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: SmithyXML.XMLReadWrite.documentWritingClosure(rootNodeInfo: .init("IdempotencyToken")), inputWritingClosure: IdempotencyTokenWithStructureInput.writingClosure(_:to:))) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) @@ -40,8 +33,6 @@ extension RestXmlProtocolClient: RestXmlProtocolClientProtocol { let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) return result } - -} """ contents.shouldContainOnlyOnce(expectedContents) } diff --git a/smithy-swift-codegen/src/test/kotlin/RecursiveShapeBoxerTests.kt b/smithy-swift-codegen/src/test/kotlin/RecursiveShapeBoxerTests.kt index d66728c80..539050bc6 100644 --- a/smithy-swift-codegen/src/test/kotlin/RecursiveShapeBoxerTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/RecursiveShapeBoxerTests.kt @@ -89,11 +89,11 @@ internal class RecursiveShapeBoxerTests { extension ExampleClientTypes { public struct RecursiveShapesInputOutputNested1: Swift.Equatable { public var foo: Swift.String? - public var nested: Box? + @Indirect public var nested: ExampleClientTypes.RecursiveShapesInputOutputNested2? public init( foo: Swift.String? = nil, - nested: Box? = nil + nested: ExampleClientTypes.RecursiveShapesInputOutputNested2? = nil ) { self.foo = foo diff --git a/smithy-swift-codegen/src/test/kotlin/ShapeValueGeneratorTest.kt b/smithy-swift-codegen/src/test/kotlin/ShapeValueGeneratorTest.kt index 3f17e6084..bee0787f2 100644 --- a/smithy-swift-codegen/src/test/kotlin/ShapeValueGeneratorTest.kt +++ b/smithy-swift-codegen/src/test/kotlin/ShapeValueGeneratorTest.kt @@ -356,10 +356,8 @@ MyStruct( var expected = """ RecursiveShapesInputOutputNested1( foo: "Foo1", - nested: Box( - value: RecursiveShapesInputOutputNested2( - bar: "Bar1" - ) + nested: RecursiveShapesInputOutputNested2( + bar: "Bar1" ) ) """.trimIndent() diff --git a/smithy-swift-codegen/src/test/kotlin/StructDecodeGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/StructDecodeGenerationTests.kt index 8dcfc543e..3768030c1 100644 --- a/smithy-swift-codegen/src/test/kotlin/StructDecodeGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/StructDecodeGenerationTests.kt @@ -464,7 +464,7 @@ extension NestedShapesOutputBody: Swift.Decodable { try encodeContainer.encode(foo, forKey: .foo) } if let nested = self.nested { - try encodeContainer.encode(nested.value, forKey: .nested) + try encodeContainer.encode(nested, forKey: .nested) } } @@ -472,7 +472,7 @@ extension NestedShapesOutputBody: Swift.Decodable { let containerValues = try decoder.container(keyedBy: CodingKeys.self) let fooDecoded = try containerValues.decodeIfPresent(Swift.String.self, forKey: .foo) foo = fooDecoded - let nestedDecoded = try containerValues.decodeIfPresent(Box.self, forKey: .nested) + let nestedDecoded = try containerValues.decodeIfPresent(ExampleClientTypes.RecursiveShapesInputOutputNested2.self, forKey: .nested) nested = nestedDecoded } } diff --git a/smithy-swift-codegen/src/test/kotlin/StructEncodeGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/StructEncodeGenerationTests.kt index d185cfaf5..63f76dda5 100644 --- a/smithy-swift-codegen/src/test/kotlin/StructEncodeGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/StructEncodeGenerationTests.kt @@ -323,7 +323,7 @@ class StructEncodeGenerationTests { try encodeContainer.encode(foo, forKey: .foo) } if let nested = self.nested { - try encodeContainer.encode(nested.value, forKey: .nested) + try encodeContainer.encode(nested, forKey: .nested) } } @@ -331,7 +331,7 @@ class StructEncodeGenerationTests { let containerValues = try decoder.container(keyedBy: CodingKeys.self) let fooDecoded = try containerValues.decodeIfPresent(Swift.String.self, forKey: .foo) foo = fooDecoded - let nestedDecoded = try containerValues.decodeIfPresent(Box.self, forKey: .nested) + let nestedDecoded = try containerValues.decodeIfPresent(RecursiveShapesInputOutputNested2.self, forKey: .nested) nested = nestedDecoded } } diff --git a/smithy-swift-codegen/src/test/kotlin/StructureGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/StructureGeneratorTests.kt index e8942cfdf..0b74e45d0 100644 --- a/smithy-swift-codegen/src/test/kotlin/StructureGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/StructureGeneratorTests.kt @@ -141,11 +141,11 @@ class StructureGeneratorTests { """ public struct RecursiveShapesInputOutputNested1: Swift.Equatable { public var foo: Swift.String? - public var nested: Box? + @Indirect public var nested: RecursiveShapesInputOutputNested2? public init( foo: Swift.String? = nil, - nested: Box? = nil + nested: RecursiveShapesInputOutputNested2? = nil ) { self.foo = foo diff --git a/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpRestXMLProtocolGenerator.kt b/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpRestXMLProtocolGenerator.kt index 087d2c5c1..6cc1dbef1 100644 --- a/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpRestXMLProtocolGenerator.kt +++ b/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpRestXMLProtocolGenerator.kt @@ -24,7 +24,6 @@ import software.amazon.smithy.swift.codegen.integration.codingKeys.CodingKeysGen import software.amazon.smithy.swift.codegen.integration.codingKeys.DefaultCodingKeysGenerator import software.amazon.smithy.swift.codegen.integration.httpResponse.HttpResponseGeneratable import software.amazon.smithy.swift.codegen.integration.httpResponse.HttpResponseGenerator -import software.amazon.smithy.swift.codegen.integration.serde.DynamicNodeEncodingGeneratorStrategy import software.amazon.smithy.swift.codegen.integration.serde.json.StructEncodeXMLGenerator import software.amazon.smithy.swift.codegen.integration.serde.xml.StructDecodeXMLGenerator import software.amazon.smithy.swift.codegen.model.ShapeMetadata @@ -56,10 +55,8 @@ class MockHttpRestXMLProtocolGenerator : HttpBindingProtocolGenerator() { defaultTimestampFormat: TimestampFormatTrait.Format, path: String? ) { - val encoder = StructEncodeXMLGenerator(ctx, shapeContainingMembers, members, writer, defaultTimestampFormat) + val encoder = StructEncodeXMLGenerator(ctx, shapeContainingMembers, members, writer) encoder.render() - val xmlNamespaces = encoder.xmlNamespaces - DynamicNodeEncodingGeneratorStrategy(ctx, shapeContainingMembers, xmlNamespaces).renderIfNeeded() } override fun renderStructDecode( ctx: ProtocolGenerator.GenerationContext, diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/AttributeEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/AttributeEncodeXMLGenerationTests.kt index cb592df27..5ff08d0ee 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/AttributeEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/AttributeEncodeXMLGenerationTests.kt @@ -13,29 +13,6 @@ import io.kotest.matchers.string.shouldContainOnlyOnce import org.junit.jupiter.api.Test class AttributeEncodeXMLGenerationTests { - @Test - fun `001 xml attributes encoding for input type`() { - val context = setupTests("Isolated/Restxml/xml-attr.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "/RestXml/models/XmlAttributesInput+DynamicNodeEncoding.swift") - val expectedContents = - """ - extension XmlAttributesInput: ClientRuntime.DynamicNodeEncoding { - public static func nodeEncoding(for key: Swift.CodingKey) -> ClientRuntime.NodeEncoding { - let codingKeys = [ - "test" - ] - if let key = key as? ClientRuntime.Key { - if codingKeys.contains(key.stringValue) { - return .attribute - } - } - return .element - } - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) - } - @Test fun `002 creates encodable`() { val context = setupTests("Isolated/Restxml/xml-attr.smithy", "aws.protocoltests.restxml#RestXml") @@ -48,14 +25,10 @@ class AttributeEncodeXMLGenerationTests { case foo } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let attr = attr { - try container.encode(attr, forKey: ClientRuntime.Key("test")) - } - if let foo = foo { - try container.encode(foo, forKey: ClientRuntime.Key("foo")) - } + static func writingClosure(_ value: XmlAttributesInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("test", location: .attribute)].write(value.attr) + try writer[.init("foo")].write(value.foo) } } """.trimIndent() diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/BlobEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/BlobEncodeXMLGenerationTests.kt index 471d1f7da..531903422 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/BlobEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/BlobEncodeXMLGenerationTests.kt @@ -24,11 +24,9 @@ class BlobEncodeXMLGenerationTests { case data } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let data = data { - try container.encode(data, forKey: ClientRuntime.Key("data")) - } + static func writingClosure(_ value: XmlBlobsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("data")].write(value.data) } } """.trimIndent() @@ -47,17 +45,9 @@ class BlobEncodeXMLGenerationTests { case nestedBlobList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedBlobList = nestedBlobList { - var nestedBlobListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedBlobList")) - for nestedbloblist0 in nestedBlobList { - var nestedbloblist0Container0 = nestedBlobListContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for blob1 in nestedbloblist0 { - try nestedbloblist0Container0.encode(blob1, forKey: ClientRuntime.Key("member")) - } - } - } + static func writingClosure(_ value: XmlBlobsNestedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedBlobList")].writeList(value.nestedBlobList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: ClientRuntime.Data.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/EnumEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/EnumEncodeXMLGenerationTests.kt index e320f3192..04be2965a 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/EnumEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/EnumEncodeXMLGenerationTests.kt @@ -27,23 +27,12 @@ class EnumEncodeXMLGenerationTests { case fooEnumList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let fooEnum1 = fooEnum1 { - try container.encode(fooEnum1, forKey: ClientRuntime.Key("fooEnum1")) - } - if let fooEnum2 = fooEnum2 { - try container.encode(fooEnum2, forKey: ClientRuntime.Key("fooEnum2")) - } - if let fooEnum3 = fooEnum3 { - try container.encode(fooEnum3, forKey: ClientRuntime.Key("fooEnum3")) - } - if let fooEnumList = fooEnumList { - var fooEnumListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("fooEnumList")) - for fooenum0 in fooEnumList { - try fooEnumListContainer.encode(fooenum0, forKey: ClientRuntime.Key("member")) - } - } + static func writingClosure(_ value: XmlEnumsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("fooEnum1")].write(value.fooEnum1) + try writer[.init("fooEnum2")].write(value.fooEnum2) + try writer[.init("fooEnum3")].write(value.fooEnum3) + try writer[.init("fooEnumList")].writeList(value.fooEnumList, memberWritingClosure: RestXmlProtocolClientTypes.FooEnum.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() @@ -61,17 +50,9 @@ class EnumEncodeXMLGenerationTests { case nestedEnumsList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedEnumsList = nestedEnumsList { - var nestedEnumsListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedEnumsList")) - for nestedenumslist0 in nestedEnumsList { - var nestedenumslist0Container0 = nestedEnumsListContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for fooenum1 in nestedenumslist0 { - try nestedenumslist0Container0.encode(fooenum1, forKey: ClientRuntime.Key("member")) - } - } - } + static func writingClosure(_ value: XmlEnumsNestedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedEnumsList")].writeList(value.nestedEnumsList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: RestXmlProtocolClientTypes.FooEnum.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/ListEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/ListEncodeXMLGenerationTests.kt index 624bfe7bb..30f42cec9 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/ListEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/ListEncodeXMLGenerationTests.kt @@ -24,14 +24,9 @@ class ListEncodeXMLGenerationTests { case renamedListMembers = "renamed" } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let renamedListMembers = renamedListMembers { - var renamedListMembersContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("renamed")) - for string0 in renamedListMembers { - try renamedListMembersContainer.encode(string0, forKey: ClientRuntime.Key("item")) - } - } + static func writingClosure(_ value: XmlListXmlNameInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("renamed")].writeList(value.renamedListMembers, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("item"), isFlattened: false) } } """.trimIndent() @@ -49,17 +44,9 @@ class ListEncodeXMLGenerationTests { case renamedListMembers = "renamed" } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let renamedListMembers = renamedListMembers { - var renamedListMembersContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("renamed")) - for renamedlistmembers0 in renamedListMembers { - var renamedlistmembers0Container0 = renamedListMembersContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("item")) - for string1 in renamedlistmembers0 { - try renamedlistmembers0Container0.encode(string1, forKey: ClientRuntime.Key("subItem")) - } - } - } + static func writingClosure(_ value: XmlListXmlNameNestedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("renamed")].writeList(value.renamedListMembers, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("subItem"), isFlattened: false), memberNodeInfo: .init("item"), isFlattened: false) } } """.trimIndent() @@ -78,17 +65,9 @@ class ListEncodeXMLGenerationTests { case nestedStringList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedStringList = nestedStringList { - var nestedStringListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedStringList")) - for stringlist0 in nestedStringList { - var stringlist0Container0 = nestedStringListContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for string1 in stringlist0 { - try stringlist0Container0.encode(string1, forKey: ClientRuntime.Key("member")) - } - } - } + static func writingClosure(_ value: XmlNestedWrappedListInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedStringList")].writeList(value.nestedStringList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() @@ -106,20 +85,9 @@ class ListEncodeXMLGenerationTests { case nestedNestedStringList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedNestedStringList = nestedNestedStringList { - var nestedNestedStringListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedNestedStringList")) - for nestedstringlist0 in nestedNestedStringList { - var nestedstringlist0Container0 = nestedNestedStringListContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for stringlist1 in nestedstringlist0 { - var stringlist1Container1 = nestedstringlist0Container0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for string2 in stringlist1 { - try stringlist1Container1.encode(string2, forKey: ClientRuntime.Key("member")) - } - } - } - } + static func writingClosure(_ value: XmlNestedNestedWrappedListInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedNestedStringList")].writeList(value.nestedNestedStringList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() @@ -137,29 +105,9 @@ class ListEncodeXMLGenerationTests { case nestedNestedStringList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedNestedStringList = nestedNestedStringList { - if nestedNestedStringList.isEmpty { - var nestedNestedStringListContainer = container.nestedUnkeyedContainer(forKey: ClientRuntime.Key("nestedNestedStringList")) - try nestedNestedStringListContainer.encodeNil() - } else { - for nestedstringlist0 in nestedNestedStringList { - if let nestedstringlist0 = nestedstringlist0 { - var nestedstringlist0Container0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedNestedStringList")) - for stringlist1 in nestedstringlist0 { - if let stringlist1 = stringlist1 { - var stringlist1Container1 = nestedstringlist0Container0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for string2 in stringlist1 { - var stringlist1Container2 = stringlist1Container1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - try stringlist1Container2.encode(string2, forKey: ClientRuntime.Key("")) - } - } - } - } - } - } - } + static func writingClosure(_ value: XmlNestedNestedFlattenedListInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedNestedStringList")].writeList(value.nestedNestedStringList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: true) } } """.trimIndent() @@ -180,32 +128,12 @@ class ListEncodeXMLGenerationTests { case stringSet } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let booleanList = booleanList { - var booleanListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("booleanList")) - for primitiveboolean0 in booleanList { - try booleanListContainer.encode(primitiveboolean0, forKey: ClientRuntime.Key("member")) - } - } - if let integerList = integerList { - var integerListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("integerList")) - for integer0 in integerList { - try integerListContainer.encode(integer0, forKey: ClientRuntime.Key("member")) - } - } - if let stringList = stringList { - var stringListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("stringList")) - for string0 in stringList { - try stringListContainer.encode(string0, forKey: ClientRuntime.Key("member")) - } - } - if let stringSet = stringSet { - var stringSetContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("stringSet")) - for string0 in stringSet { - try stringSetContainer.encode(string0, forKey: ClientRuntime.Key("member")) - } - } + static func writingClosure(_ value: XmlEmptyListsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("booleanList")].writeList(value.booleanList, memberWritingClosure: Swift.Bool.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) + try writer[.init("integerList")].writeList(value.integerList, memberWritingClosure: Swift.Int.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) + try writer[.init("stringList")].writeList(value.stringList, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) + try writer[.init("stringSet")].writeList(value.stringSet, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() @@ -223,14 +151,9 @@ class ListEncodeXMLGenerationTests { case myGroceryList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myGroceryList = myGroceryList { - var myGroceryListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myGroceryList")) - for string0 in myGroceryList { - try myGroceryListContainer.encode(string0, forKey: ClientRuntime.Key("member")) - } - } + static func writingClosure(_ value: XmlWrappedListInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myGroceryList")].writeList(value.myGroceryList, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() @@ -249,43 +172,9 @@ class ListEncodeXMLGenerationTests { case myGroceryList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myGroceryList = myGroceryList { - if myGroceryList.isEmpty { - var myGroceryListContainer = container.nestedUnkeyedContainer(forKey: ClientRuntime.Key("myGroceryList")) - try myGroceryListContainer.encodeNil() - } else { - for string0 in myGroceryList { - var myGroceryListContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myGroceryList")) - try myGroceryListContainer0.encode(string0, forKey: ClientRuntime.Key("")) - } - } - } - } - } - """.trimIndent() - - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `009 encode nested flattened date time with namespace`() { - val context = setupTests("Isolated/Restxml/xml-lists-flattened-nested-datetime.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "/RestXml/models/XmlTimestampsNestedFlattenedInput+DynamicNodeEncoding.swift") - val expectedContents = - """ - extension XmlTimestampsNestedFlattenedInput: ClientRuntime.DynamicNodeEncoding { - public static func nodeEncoding(for key: Swift.CodingKey) -> ClientRuntime.NodeEncoding { - let xmlNamespaceValues = [ - "xmlns:baz" - ] - if let key = key as? ClientRuntime.Key { - if xmlNamespaceValues.contains(key.stringValue) { - return .attribute - } - } - return .element + static func writingClosure(_ value: XmlFlattenedListInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myGroceryList")].writeList(value.myGroceryList, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: true) } } """.trimIndent() @@ -304,25 +193,9 @@ class ListEncodeXMLGenerationTests { case nestedTimestampList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedTimestampList = nestedTimestampList { - if nestedTimestampList.isEmpty { - var nestedTimestampListContainer = container.nestedUnkeyedContainer(forKey: ClientRuntime.Key("nestedTimestampList")) - try nestedTimestampListContainer.encodeNil() - } else { - for nestedtimestamplist0 in nestedTimestampList { - if let nestedtimestamplist0 = nestedtimestamplist0 { - var nestedtimestamplist0Container0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedTimestampList")) - for timestamp1 in nestedtimestamplist0 { - var nestedtimestamplist0Container1 = nestedtimestamplist0Container0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedMember")) - try nestedtimestamplist0Container1.encode("http://baz.com", forKey: ClientRuntime.Key("xmlns:baz")) - try nestedtimestamplist0Container1.encodeTimestamp(timestamp1, format: .epochSeconds, forKey: Key("")) - } - } - } - } - } + static func writingClosure(_ value: XmlTimestampsNestedFlattenedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedTimestampList")].writeList(value.nestedTimestampList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: SmithyXML.timestampWritingClosure(format: .epochSeconds), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("nestedMember", namespace: .init(prefix: "baz", uri: "http://baz.com")), isFlattened: true) } } """.trimIndent() @@ -343,42 +216,12 @@ class ListEncodeXMLGenerationTests { case stringSet } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let booleanList = booleanList { - var booleanListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("booleanList")) - for primitiveboolean0 in booleanList { - try booleanListContainer.encode(primitiveboolean0, forKey: ClientRuntime.Key("member")) - } - } - if let integerList = integerList { - var integerListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("integerList")) - for integer0 in integerList { - try integerListContainer.encode(integer0, forKey: ClientRuntime.Key("member")) - } - } - if let stringList = stringList { - if stringList.isEmpty { - var stringListContainer = container.nestedUnkeyedContainer(forKey: ClientRuntime.Key("stringList")) - try stringListContainer.encodeNil() - } else { - for string0 in stringList { - var stringListContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("stringList")) - try stringListContainer0.encode(string0, forKey: ClientRuntime.Key("")) - } - } - } - if let stringSet = stringSet { - if stringSet.isEmpty { - var stringSetContainer = container.nestedUnkeyedContainer(forKey: ClientRuntime.Key("stringSet")) - try stringSetContainer.encodeNil() - } else { - for string0 in stringSet { - var stringSetContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("stringSet")) - try stringSetContainer0.encode(string0, forKey: ClientRuntime.Key("")) - } - } - } + static func writingClosure(_ value: XmlEmptyFlattenedListsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("booleanList")].writeList(value.booleanList, memberWritingClosure: Swift.Bool.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) + try writer[.init("integerList")].writeList(value.integerList, memberWritingClosure: Swift.Int.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) + try writer[.init("stringList")].writeList(value.stringList, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: true) + try writer[.init("stringSet")].writeList(value.stringSet, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: true) } } """.trimIndent() @@ -397,24 +240,9 @@ class ListEncodeXMLGenerationTests { case nestedList = "listOfNestedStrings" } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedList = nestedList { - if nestedList.isEmpty { - var nestedListContainer = container.nestedUnkeyedContainer(forKey: ClientRuntime.Key("listOfNestedStrings")) - try nestedListContainer.encodeNil() - } else { - for nestedstringmember0 in nestedList { - if let nestedstringmember0 = nestedstringmember0 { - var nestedstringmember0Container0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("listOfNestedStrings")) - for string1 in nestedstringmember0 { - var nestedstringmember0Container1 = nestedstringmember0Container0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedMember")) - try nestedstringmember0Container1.encode(string1, forKey: ClientRuntime.Key("")) - } - } - } - } - } + static func writingClosure(_ value: XmlListNestedFlattenedXmlNameInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("listOfNestedStrings")].writeList(value.nestedList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("nestedNestedMember"), isFlattened: false), memberNodeInfo: .init("nestedMember"), isFlattened: true) } } """.trimIndent() @@ -433,23 +261,9 @@ class ListEncodeXMLGenerationTests { case myList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myList = myList { - var myListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myList")) - for mysimplemap0 in myList { - var myListContainer0 = myListContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - if let mysimplemap0 = mysimplemap0 { - for (stringKey0, stringValue0) in mysimplemap0 { - var entryContainer0 = myListContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer0.encode(stringValue0, forKey: ClientRuntime.Key("")) - } - } - } - } + static func writingClosure(_ value: XmlListContainMapInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myList")].writeList(value.myList, memberWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: Swift.String.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() @@ -467,27 +281,9 @@ class ListEncodeXMLGenerationTests { case myList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myList = myList { - if myList.isEmpty { - var myListContainer = container.nestedUnkeyedContainer(forKey: ClientRuntime.Key("myList")) - try myListContainer.encodeNil() - } else { - for mysimplemap0 in myList { - var myListContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myList")) - if let mysimplemap0 = mysimplemap0 { - for (stringKey0, stringValue0) in mysimplemap0 { - var entryContainer0 = myListContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer0.encode(stringValue0, forKey: ClientRuntime.Key("")) - } - } - } - } - } + static func writingClosure(_ value: XmlListFlattenedContainMapInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myList")].writeList(value.myList, memberWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: Swift.String.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: true) } } """.trimIndent() diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/MapEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/MapEncodeXMLGenerationTests.kt index d3b68ce39..93a9cec38 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/MapEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/MapEncodeXMLGenerationTests.kt @@ -24,18 +24,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - var myMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - for (stringKey0, greetingstructValue0) in myMap { - var entryContainer0 = myMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer0.encode(greetingstructValue0, forKey: ClientRuntime.Key("")) - } - } + static func writingClosure(_ value: XmlMapsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false) } } """.trimIndent() @@ -53,18 +44,9 @@ class MapEncodeXMLGenerationTests { case `protocol` = "protocol" } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let `protocol` = `protocol` { - var protocolContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("protocol")) - for (stringKey0, greetingstructValue0) in `protocol` { - var entryContainer0 = protocolContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer0.encode(greetingstructValue0, forKey: ClientRuntime.Key("")) - } - } + static func writingClosure(_ value: XmlMapsWithNameProtocolInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("protocol")].writeMap(value.`protocol`, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false) } } """.trimIndent() @@ -82,24 +64,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - var myMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - for (stringKey0, xmlmapsnestednestedinputoutputmapValue0) in myMap { - var entryContainer0 = myMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - for (stringKey1, greetingstructValue1) in xmlmapsnestednestedinputoutputmapValue0 { - var entryContainer1 = valueContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer1 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer1.encode(stringKey1, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer1.encode(greetingstructValue1, forKey: ClientRuntime.Key("")) - } - } - } + static func writingClosure(_ value: XmlMapsNestedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false) } } """.trimIndent() @@ -117,30 +84,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - var myMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - for (stringKey0, xmlmapsnestednestedinputoutputmapValue0) in myMap { - var entryContainer0 = myMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - for (stringKey1, xmlmapsnestednestednestedinputoutputmapValue1) in xmlmapsnestednestedinputoutputmapValue0 { - var entryContainer1 = valueContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer1 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer1.encode(stringKey1, forKey: ClientRuntime.Key("")) - var valueContainer2 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - for (stringKey2, greetingstructValue2) in xmlmapsnestednestednestedinputoutputmapValue1 { - var entryContainer2 = valueContainer2.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer2 = entryContainer2.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer2.encode(stringKey2, forKey: ClientRuntime.Key("")) - var valueContainer2 = entryContainer2.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer2.encode(greetingstructValue2, forKey: ClientRuntime.Key("")) - } - } - } - } + static func writingClosure(_ value: XmlMapsNestedNestedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false) } } """.trimIndent() @@ -158,21 +104,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - if myMap.isEmpty { - let _ = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - } else { - for (stringKey0, greetingstructValue0) in myMap { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - var keyContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer0.encode(greetingstructValue0, forKey: ClientRuntime.Key("")) - } - } - } + static func writingClosure(_ value: XmlFlattenedMapsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: true) } } """.trimIndent() @@ -190,27 +124,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - if myMap.isEmpty { - let _ = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - } else { - for (stringKey0, xmlmapsnestednestedinputoutputmapValue0) in myMap { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - var keyContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - for (stringKey1, greetingstructValue1) in xmlmapsnestednestedinputoutputmapValue0 { - var nestedContainer1 = valueContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer1 = nestedContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer1.encode(stringKey1, forKey: ClientRuntime.Key("")) - var valueContainer1 = nestedContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer1.encode(greetingstructValue1, forKey: ClientRuntime.Key("")) - } - } - } - } + static func writingClosure(_ value: XmlMapsFlattenedNestedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: true) } } """.trimIndent() @@ -228,18 +144,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - var myMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - for (stringKey0, greetingstructValue0) in myMap { - var entryContainer0 = myMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("Attribute")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("Setting")) - try valueContainer0.encode(greetingstructValue0, forKey: ClientRuntime.Key("")) - } - } + static func writingClosure(_ value: XmlMapsXmlNameInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.writingClosure(_:to:), keyNodeInfo: .init("Attribute"), valueNodeInfo: .init("Setting"), isFlattened: false) } } """.trimIndent() @@ -257,21 +164,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - if myMap.isEmpty { - let _ = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - } else { - for (stringKey0, greetingstructValue0) in myMap { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - var keyContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("SomeCustomKey")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("SomeCustomValue")) - try valueContainer0.encode(greetingstructValue0, forKey: ClientRuntime.Key("")) - } - } - } + static func writingClosure(_ value: XmlMapsXmlNameFlattenedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.writingClosure(_:to:), keyNodeInfo: .init("SomeCustomKey"), valueNodeInfo: .init("SomeCustomValue"), isFlattened: true) } } """.trimIndent() @@ -289,24 +184,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - var myMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - for (stringKey0, xmlmapsnestednestedinputoutputmapValue0) in myMap { - var entryContainer0 = myMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("CustomKey1")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("CustomValue1")) - for (stringKey1, greetingstructValue1) in xmlmapsnestednestedinputoutputmapValue0 { - var entryContainer1 = valueContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer1 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("CustomKey2")) - try keyContainer1.encode(stringKey1, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("CustomValue2")) - try valueContainer1.encode(greetingstructValue1, forKey: ClientRuntime.Key("")) - } - } - } + static func writingClosure(_ value: XmlMapsXmlNameNestedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.GreetingStruct.writingClosure(_:to:), keyNodeInfo: .init("CustomKey2"), valueNodeInfo: .init("CustomValue2"), isFlattened: false), keyNodeInfo: .init("CustomKey1"), valueNodeInfo: .init("CustomValue1"), isFlattened: false) } } """.trimIndent() @@ -324,27 +204,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - if myMap.isEmpty { - let _ = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - } else { - for (stringKey0, xmlmapsnestednestedinputoutputmapValue0) in myMap { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - var keyContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("yek")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("eulav")) - for (stringKey1, stringValue1) in xmlmapsnestednestedinputoutputmapValue0 { - var nestedContainer1 = valueContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer1 = nestedContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("K")) - try keyContainer1.encode(stringKey1, forKey: ClientRuntime.Key("")) - var valueContainer1 = nestedContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("V")) - try valueContainer1.encode(stringValue1, forKey: ClientRuntime.Key("")) - } - } - } - } + static func writingClosure(_ value: XmlMapsFlattenedNestedXmlNameInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: Swift.String.writingClosure(_:to:), keyNodeInfo: .init("K"), valueNodeInfo: .init("V"), isFlattened: false), keyNodeInfo: .init("yek"), valueNodeInfo: .init("eulav"), isFlattened: true) } } """.trimIndent() @@ -362,24 +224,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("http://aoo.com", forKey: ClientRuntime.Key("xmlns")) - } - if let myMap = myMap { - var myMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - try myMapContainer.encode("http://boo.com", forKey: ClientRuntime.Key("xmlns")) - for (stringKey0, stringValue0) in myMap { - var entryContainer0 = myMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("Quality")) - try keyContainer0.encode("http://doo.com", forKey: ClientRuntime.Key("xmlns")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("Degree")) - try valueContainer0.encode("http://eoo.com", forKey: ClientRuntime.Key("xmlns")) - try valueContainer0.encode(stringValue0, forKey: ClientRuntime.Key("")) - } - } + static func writingClosure(_ value: XmlMapsXmlNamespaceInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap", namespace: .init(prefix: "", uri: "http://boo.com"))].writeMap(value.myMap, valueWritingClosure: Swift.String.writingClosure(_:to:), keyNodeInfo: .init("Quality", namespace: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("Degree", namespace: .init(prefix: "", uri: "http://eoo.com")), isFlattened: false) } } """.trimIndent() @@ -396,27 +243,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("http://aoo.com", forKey: ClientRuntime.Key("xmlns")) - } - if let myMap = myMap { - if myMap.isEmpty { - let _ = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - } else { - for (stringKey0, stringValue0) in myMap { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - try nestedContainer0.encode("http://boo.com", forKey: ClientRuntime.Key("xmlns")) - var keyContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("Uid")) - try keyContainer0.encode("http://doo.com", forKey: ClientRuntime.Key("xmlns")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("Val")) - try valueContainer0.encode("http://eoo.com", forKey: ClientRuntime.Key("xmlns")) - try valueContainer0.encode(stringValue0, forKey: ClientRuntime.Key("")) - } - } - } + static func writingClosure(_ value: XmlMapsFlattenedXmlNamespaceInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap", namespace: .init(prefix: "", uri: "http://boo.com"))].writeMap(value.myMap, valueWritingClosure: Swift.String.writingClosure(_:to:), keyNodeInfo: .init("Uid", namespace: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("Val", namespace: .init(prefix: "", uri: "http://eoo.com")), isFlattened: true) } } """.trimIndent() @@ -434,32 +263,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("http://aoo.com", forKey: ClientRuntime.Key("xmlns")) - } - if let myMap = myMap { - var myMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - try myMapContainer.encode("http://boo.com", forKey: ClientRuntime.Key("xmlns")) - for (stringKey0, xmlmapsnestednestedxmlnamespaceinputoutputmapValue0) in myMap { - var entryContainer0 = myMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("yek")) - try keyContainer0.encode("http://doo.com", forKey: ClientRuntime.Key("xmlns")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("eulav")) - try valueContainer1.encode("http://eoo.com", forKey: ClientRuntime.Key("xmlns")) - for (stringKey1, stringValue1) in xmlmapsnestednestedxmlnamespaceinputoutputmapValue0 { - var entryContainer1 = valueContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer1 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("K")) - try keyContainer1.encode("http://goo.com", forKey: ClientRuntime.Key("xmlns")) - try keyContainer1.encode(stringKey1, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("V")) - try valueContainer1.encode("http://hoo.com", forKey: ClientRuntime.Key("xmlns")) - try valueContainer1.encode(stringValue1, forKey: ClientRuntime.Key("")) - } - } - } + static func writingClosure(_ value: XmlMapsNestedXmlNamespaceInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap", namespace: .init(prefix: "", uri: "http://boo.com"))].writeMap(value.myMap, valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: Swift.String.writingClosure(_:to:), keyNodeInfo: .init("K", namespace: .init(prefix: "", uri: "http://goo.com")), valueNodeInfo: .init("V", namespace: .init(prefix: "", uri: "http://hoo.com")), isFlattened: false), keyNodeInfo: .init("yek", namespace: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("eulav", namespace: .init(prefix: "", uri: "http://eoo.com")), isFlattened: false) } } """.trimIndent() @@ -476,34 +282,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("http://aoo.com", forKey: ClientRuntime.Key("xmlns")) - } - if let myMap = myMap { - if myMap.isEmpty { - let _ = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - } else { - for (stringKey0, xmlmapsnestednestednamespaceinputoutputmapValue0) in myMap { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - var keyContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("yek")) - try keyContainer0.encode("http://doo.com", forKey: ClientRuntime.Key("xmlns")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("eulav")) - try valueContainer1.encode("http://eoo.com", forKey: ClientRuntime.Key("xmlns")) - for (stringKey1, stringValue1) in xmlmapsnestednestednamespaceinputoutputmapValue0 { - var nestedContainer1 = valueContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer1 = nestedContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("K")) - try keyContainer1.encode("http://goo.com", forKey: ClientRuntime.Key("xmlns")) - try keyContainer1.encode(stringKey1, forKey: ClientRuntime.Key("")) - var valueContainer1 = nestedContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("V")) - try valueContainer1.encode("http://hoo.com", forKey: ClientRuntime.Key("xmlns")) - try valueContainer1.encode(stringValue1, forKey: ClientRuntime.Key("")) - } - } - } - } + static func writingClosure(_ value: XmlMapsFlattenedNestedXmlNamespaceInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap", namespace: .init(prefix: "", uri: "http://boo.com"))].writeMap(value.myMap, valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: Swift.String.writingClosure(_:to:), keyNodeInfo: .init("K", namespace: .init(prefix: "", uri: "http://goo.com")), valueNodeInfo: .init("V", namespace: .init(prefix: "", uri: "http://hoo.com")), isFlattened: false), keyNodeInfo: .init("yek", namespace: .init(prefix: "", uri: "http://doo.com")), valueNodeInfo: .init("eulav", namespace: .init(prefix: "", uri: "http://eoo.com")), isFlattened: true) } } """.trimIndent() @@ -520,20 +301,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - var myMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - for (stringKey0, xmlsimplestringlistValue0) in myMap { - var entryContainer0 = myMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - for string1 in xmlsimplestringlistValue0 { - try valueContainer1.encode(string1, forKey: ClientRuntime.Key("member")) - } - } - } + static func writingClosure(_ value: XmlMapsContainListInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false) } } """.trimIndent() @@ -550,23 +320,9 @@ class MapEncodeXMLGenerationTests { case myMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let myMap = myMap { - if myMap.isEmpty { - let _ = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - } else { - for (stringKey0, xmlsimplestringlistValue0) in myMap { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("myMap")) - var keyContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - for string1 in xmlsimplestringlistValue0 { - try valueContainer1.encode(string1, forKey: ClientRuntime.Key("member")) - } - } - } - } + static func writingClosure(_ value: XmlMapsFlattenedContainListInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("myMap")].writeMap(value.myMap, valueWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: true) } } """.trimIndent() @@ -583,18 +339,9 @@ class MapEncodeXMLGenerationTests { case timestampMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let timestampMap = timestampMap { - var timestampMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("timestampMap")) - for (stringKey0, timestampValue0) in timestampMap { - var entryContainer0 = timestampMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer0.encodeTimestamp(timestampValue0, format: .epochSeconds, forKey: ClientRuntime.Key("")) - } - } + static func writingClosure(_ value: XmlMapsTimestampsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("timestampMap")].writeMap(value.timestampMap, valueWritingClosure: SmithyXML.timestampWritingClosure(format: .epochSeconds), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false) } } """.trimIndent() @@ -612,21 +359,9 @@ class MapEncodeXMLGenerationTests { case timestampMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let timestampMap = timestampMap { - if timestampMap.isEmpty { - let _ = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("timestampMap")) - } else { - for (stringKey0, timestampValue0) in timestampMap { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("timestampMap")) - var keyContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer0.encodeTimestamp(timestampValue0, format: .epochSeconds, forKey: ClientRuntime.Key("")) - } - } - } + static func writingClosure(_ value: XmlMapsFlattenedTimestampsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("timestampMap")].writeMap(value.timestampMap, valueWritingClosure: SmithyXML.timestampWritingClosure(format: .epochSeconds), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: true) } } """.trimIndent() @@ -645,43 +380,10 @@ class MapEncodeXMLGenerationTests { case nestedMap } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let flatNestedMap = flatNestedMap { - if flatNestedMap.isEmpty { - let _ = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("flatNestedMap")) - } else { - for (stringKey0, fooenummapValue0) in flatNestedMap { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("flatNestedMap")) - var keyContainer0 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = nestedContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - for (stringKey1, fooenumValue1) in fooenummapValue0 { - var nestedContainer1 = valueContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer1 = nestedContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer1.encode(stringKey1, forKey: ClientRuntime.Key("")) - var valueContainer1 = nestedContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer1.encode(fooenumValue1, forKey: ClientRuntime.Key("")) - } - } - } - } - if let nestedMap = nestedMap { - var nestedMapContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedMap")) - for (stringKey0, fooenummapValue0) in nestedMap { - var entryContainer0 = nestedMapContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - for (stringKey1, fooenumValue1) in fooenummapValue0 { - var entryContainer1 = valueContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer1 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("key")) - try keyContainer1.encode(stringKey1, forKey: ClientRuntime.Key("")) - var valueContainer1 = entryContainer1.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("value")) - try valueContainer1.encode(fooenumValue1, forKey: ClientRuntime.Key("")) - } - } - } + static func writingClosure(_ value: NestedXmlMapsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("flatNestedMap")].writeMap(value.flatNestedMap, valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.FooEnum.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: true) + try writer[.init("nestedMap")].writeMap(value.nestedMap, valueWritingClosure: SmithyXML.mapWritingClosure(valueWritingClosure: RestXmlProtocolClientTypes.FooEnum.writingClosure(_:to:), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false), keyNodeInfo: .init("key"), valueNodeInfo: .init("value"), isFlattened: false) } } """.trimIndent() diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/NamespaceEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/NamespaceEncodeXMLGenerationTests.kt index 8b22b3161..b4d3324ac 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/NamespaceEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/NamespaceEncodeXMLGenerationTests.kt @@ -17,47 +17,18 @@ class NamespaceEncodeXMLGenerationTests { fun `001 xmlnamespace, XmlNamespacesInput, Encodable`() { val context = setupTests("Isolated/Restxml/xml-namespace.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/models/XmlNamespacesInput+Encodable.swift") - val expectedContents = - """ - extension XmlNamespacesInput: Swift.Encodable { - enum CodingKeys: Swift.String, Swift.CodingKey { - case nested - } - - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("http://foo.com", forKey: ClientRuntime.Key("xmlns")) - } - if let nested = nested { - try container.encode(nested, forKey: ClientRuntime.Key("nested")) - } - } - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) + val expectedContents = """ +extension XmlNamespacesInput: Swift.Encodable { + enum CodingKeys: Swift.String, Swift.CodingKey { + case nested } - @Test - fun `002 xmlnamespace, XmlNamespacesInput, DynamicNodeEncoding`() { - val context = setupTests("Isolated/Restxml/xml-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "/RestXml/models/XmlNamespacesInput+DynamicNodeEncoding.swift") - val expectedContents = - """ - extension XmlNamespacesInput: ClientRuntime.DynamicNodeEncoding { - public static func nodeEncoding(for key: Swift.CodingKey) -> ClientRuntime.NodeEncoding { - let xmlNamespaceValues = [ - "xmlns" - ] - if let key = key as? ClientRuntime.Key { - if xmlNamespaceValues.contains(key.stringValue) { - return .attribute - } - } - return .element - } - } - """.trimIndent() + static func writingClosure(_ value: XmlNamespacesInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nested", namespace: .init(prefix: "", uri: "http://boo.com"))].write(value.nested, writingClosure: RestXmlProtocolClientTypes.XmlNamespaceNested.writingClosure(_:to:)) + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -73,25 +44,10 @@ class NamespaceEncodeXMLGenerationTests { case values } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("http://boo.com", forKey: ClientRuntime.Key("xmlns")) - } - if let foo = foo { - var fooContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("foo")) - try fooContainer.encode(foo, forKey: ClientRuntime.Key("")) - try fooContainer.encode("http://baz.com", forKey: ClientRuntime.Key("xmlns:baz")) - } - if let values = values { - var valuesContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("values")) - try valuesContainer.encode("http://qux.com", forKey: ClientRuntime.Key("xmlns")) - for string0 in values { - var valuesContainer0 = valuesContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - try valuesContainer0.encode(string0, forKey: ClientRuntime.Key("")) - try valuesContainer0.encode("http://bux.com", forKey: ClientRuntime.Key("xmlns")) - } - } + static func writingClosure(_ value: RestXmlProtocolClientTypes.XmlNamespaceNested?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("foo", namespace: .init(prefix: "baz", uri: "http://baz.com"))].write(value.foo) + try writer[.init("values", namespace: .init(prefix: "", uri: "http://qux.com"))].writeList(value.values, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member", namespace: .init(prefix: "", uri: "http://bux.com")), isFlattened: false) } public init(from decoder: Swift.Decoder) throws { @@ -123,30 +79,6 @@ class NamespaceEncodeXMLGenerationTests { contents.shouldContainOnlyOnce(expectedContents) } - @Test - fun `004 xmlnamespace, XmlNamespaceNested, nested structure needs dynamic node encoding`() { - val context = setupTests("Isolated/Restxml/xml-namespace.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "/RestXml/models/XmlNamespaceNested+DynamicNodeEncoding.swift") - val expectedContents = - """ - extension RestXmlProtocolClientTypes.XmlNamespaceNested: ClientRuntime.DynamicNodeEncoding { - public static func nodeEncoding(for key: Swift.CodingKey) -> ClientRuntime.NodeEncoding { - let xmlNamespaceValues = [ - "xmlns", - "xmlns:baz" - ] - if let key = key as? ClientRuntime.Key { - if xmlNamespaceValues.contains(key.stringValue) { - return .attribute - } - } - return .element - } - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) - } - @Test fun `005 xmlnamespace nested list, Encodable`() { val context = setupTests("Isolated/Restxml/xml-namespace-nestedlist.smithy", "aws.protocoltests.restxml#RestXml") @@ -158,49 +90,9 @@ class NamespaceEncodeXMLGenerationTests { case nested } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("http://foo.com", forKey: ClientRuntime.Key("xmlns")) - } - if let nested = nested { - var nestedContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nested")) - try nestedContainer.encode("http://aux.com", forKey: ClientRuntime.Key("xmlns")) - for xmlnestednamespacedlist0 in nested { - var xmlnestednamespacedlist0Container0 = nestedContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - try xmlnestednamespacedlist0Container0.encode("http://bux.com", forKey: ClientRuntime.Key("xmlns:baz")) - for string1 in xmlnestednamespacedlist0 { - var xmlnestednamespacedlist0Container1 = xmlnestednamespacedlist0Container0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - try xmlnestednamespacedlist0Container1.encode(string1, forKey: ClientRuntime.Key("")) - try xmlnestednamespacedlist0Container1.encode("http://bar.com", forKey: ClientRuntime.Key("xmlns:bzzzz")) - } - } - } - } - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `006 xmlnamespace nested list, dynamic node encoding`() { - val context = setupTests("Isolated/Restxml/xml-namespace-nestedlist.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "/RestXml/models/XmlNamespaceNestedListInput+DynamicNodeEncoding.swift") - val expectedContents = - """ - extension XmlNamespaceNestedListInput: ClientRuntime.DynamicNodeEncoding { - public static func nodeEncoding(for key: Swift.CodingKey) -> ClientRuntime.NodeEncoding { - let xmlNamespaceValues = [ - "xmlns", - "xmlns:baz", - "xmlns:bzzzz" - ] - if let key = key as? ClientRuntime.Key { - if xmlNamespaceValues.contains(key.stringValue) { - return .attribute - } - } - return .element + static func writingClosure(_ value: XmlNamespaceNestedListInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nested", namespace: .init(prefix: "", uri: "http://aux.com"))].writeList(value.nested, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member", namespace: .init(prefix: "bzzzz", uri: "http://bar.com")), isFlattened: false), memberNodeInfo: .init("member", namespace: .init(prefix: "baz", uri: "http://bux.com")), isFlattened: false) } } """.trimIndent() @@ -218,71 +110,9 @@ class NamespaceEncodeXMLGenerationTests { case nested } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("http://foo.com", forKey: ClientRuntime.Key("xmlns")) - } - if let nested = nested { - if nested.isEmpty { - var nestedContainer = container.nestedUnkeyedContainer(forKey: ClientRuntime.Key("nested")) - try nestedContainer.encodeNil() - } else { - for string0 in nested { - var nestedContainer0 = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nested")) - try nestedContainer0.encode("http://aux.com", forKey: ClientRuntime.Key("xmlns:baz")) - try nestedContainer0.encode(string0, forKey: ClientRuntime.Key("")) - } - } - } - } - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `008 xmlnamespace nested flattened list, dynamic node encoding`() { - val context = setupTests("Isolated/Restxml/xml-namespace-flattenedlist.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "/RestXml/models/XmlNamespaceFlattenedListInput+DynamicNodeEncoding.swift") - val expectedContents = - """ - extension XmlNamespaceFlattenedListInput: ClientRuntime.DynamicNodeEncoding { - public static func nodeEncoding(for key: Swift.CodingKey) -> ClientRuntime.NodeEncoding { - let xmlNamespaceValues = [ - "xmlns", - "xmlns:baz" - ] - if let key = key as? ClientRuntime.Key { - if xmlNamespaceValues.contains(key.stringValue) { - return .attribute - } - } - return .element - } - } - """.trimIndent() - contents.shouldContainOnlyOnce(expectedContents) - } - - @Test - fun `009 xmlnamespace on service, dynamic node encoding`() { - val context = setupTests("Isolated/Restxml/xml-namespace-onservice.smithy", "aws.protocoltests.restxml#RestXml") - val contents = getFileContents(context.manifest, "/RestXml/models/XmlNamespacesOnServiceInput+DynamicNodeEncoding.swift") - val expectedContents = - """ - extension XmlNamespacesOnServiceInput: ClientRuntime.DynamicNodeEncoding { - public static func nodeEncoding(for key: Swift.CodingKey) -> ClientRuntime.NodeEncoding { - let xmlNamespaceValues = [ - "xmlns", - "xmlns:xsi" - ] - if let key = key as? ClientRuntime.Key { - if xmlNamespaceValues.contains(key.stringValue) { - return .attribute - } - } - return .element + static func writingClosure(_ value: XmlNamespaceFlattenedListInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nested", namespace: .init(prefix: "baz", uri: "http://aux.com"))].writeList(value.nested, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: true) } } """.trimIndent() @@ -293,30 +123,20 @@ class NamespaceEncodeXMLGenerationTests { fun `010 xmlnamespace on service, encodable`() { val context = setupTests("Isolated/Restxml/xml-namespace-onservice.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/models/XmlNamespacesOnServiceInput+Encodable.swift") - val expectedContents = - """ - extension XmlNamespacesOnServiceInput: Swift.Encodable { - enum CodingKeys: Swift.String, Swift.CodingKey { - case foo - case nested - } - - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("https://example.com", forKey: ClientRuntime.Key("xmlns")) - } - if let foo = foo { - try container.encode(foo, forKey: ClientRuntime.Key("foo")) - } - if let nested = nested { - var nestedContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nested")) - try nestedContainer.encode(nested, forKey: ClientRuntime.Key("")) - try nestedContainer.encode("https://example.com", forKey: ClientRuntime.Key("xmlns:xsi")) - } - } - } - """.trimIndent() + val expectedContents = """ +extension XmlNamespacesOnServiceInput: Swift.Encodable { + enum CodingKeys: Swift.String, Swift.CodingKey { + case foo + case nested + } + + static func writingClosure(_ value: XmlNamespacesOnServiceInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("foo")].write(value.foo) + try writer[.init("nested", namespace: .init(prefix: "xsi", uri: "https://example.com"))].write(value.nested, writingClosure: RestXmlProtocolClientTypes.NestedWithNamespace.writingClosure(_:to:)) + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -324,30 +144,20 @@ class NamespaceEncodeXMLGenerationTests { fun `011 xmlnamespace on service, encodable`() { val context = setupTests("Isolated/Restxml/xml-namespace-onservice-overridable.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/models/XmlNamespacesOnServiceOverridableInput+Encodable.swift") - val expectedContents = - """ - extension XmlNamespacesOnServiceOverridableInput: Swift.Encodable { - enum CodingKeys: Swift.String, Swift.CodingKey { - case foo - case nested - } - - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if encoder.codingPath.isEmpty { - try container.encode("https://overridable.com", forKey: ClientRuntime.Key("xmlns")) - } - if let foo = foo { - try container.encode(foo, forKey: ClientRuntime.Key("foo")) - } - if let nested = nested { - var nestedContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nested")) - try nestedContainer.encode(nested, forKey: ClientRuntime.Key("")) - try nestedContainer.encode("https://example.com", forKey: ClientRuntime.Key("xmlns:xsi")) - } - } - } - """.trimIndent() + val expectedContents = """ +extension XmlNamespacesOnServiceOverridableInput: Swift.Encodable { + enum CodingKeys: Swift.String, Swift.CodingKey { + case foo + case nested + } + + static func writingClosure(_ value: XmlNamespacesOnServiceOverridableInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("foo")].write(value.foo) + try writer[.init("nested", namespace: .init(prefix: "xsi", uri: "https://example.com"))].write(value.nested, writingClosure: RestXmlProtocolClientTypes.NestedWithNamespace.writingClosure(_:to:)) + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/RecursiveShapesEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/RecursiveShapesEncodeXMLGenerationTests.kt index 2986417ad..812326e27 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/RecursiveShapesEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/RecursiveShapesEncodeXMLGenerationTests.kt @@ -17,33 +17,28 @@ class RecursiveShapesEncodeXMLGenerationTests { fun `001 encode recursive shape Nested1`() { val context = setupTests("Isolated/Restxml/xml-recursive.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/models/RecursiveShapesInputOutputNested1+Codable.swift") - val expectedContents = - """ - extension RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1: Swift.Codable { - enum CodingKeys: Swift.String, Swift.CodingKey { - case foo - case nested - } - - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let foo = foo { - try container.encode(foo, forKey: ClientRuntime.Key("foo")) - } - if let nested = nested { - try container.encode(nested, forKey: ClientRuntime.Key("nested")) - } - } - - public init(from decoder: Swift.Decoder) throws { - let containerValues = try decoder.container(keyedBy: CodingKeys.self) - let fooDecoded = try containerValues.decodeIfPresent(Swift.String.self, forKey: .foo) - foo = fooDecoded - let nestedDecoded = try containerValues.decodeIfPresent(Box.self, forKey: .nested) - nested = nestedDecoded - } - } - """.trimIndent() + val expectedContents = """ +extension RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1: Swift.Codable { + enum CodingKeys: Swift.String, Swift.CodingKey { + case foo + case nested + } + + static func writingClosure(_ value: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("foo")].write(value.foo) + try writer[.init("nested")].write(value.nested, writingClosure: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2.writingClosure(_:to:)) + } + + public init(from decoder: Swift.Decoder) throws { + let containerValues = try decoder.container(keyedBy: CodingKeys.self) + let fooDecoded = try containerValues.decodeIfPresent(Swift.String.self, forKey: .foo) + foo = fooDecoded + let nestedDecoded = try containerValues.decodeIfPresent(RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2.self, forKey: .nested) + nested = nestedDecoded + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -51,34 +46,28 @@ class RecursiveShapesEncodeXMLGenerationTests { fun `encode recursive shape Nested2`() { val context = setupTests("Isolated/Restxml/xml-recursive.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/models/RecursiveShapesInputOutputNested2+Codable.swift") - val expectedContents = - """ - extension RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2: Swift.Codable { - enum CodingKeys: Swift.String, Swift.CodingKey { - case bar - case recursiveMember - } - - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let bar = bar { - try container.encode(bar, forKey: ClientRuntime.Key("bar")) - } - if let recursiveMember = recursiveMember { - try container.encode(recursiveMember, forKey: ClientRuntime.Key("recursiveMember")) - } - } - - public init(from decoder: Swift.Decoder) throws { - let containerValues = try decoder.container(keyedBy: CodingKeys.self) - let barDecoded = try containerValues.decodeIfPresent(Swift.String.self, forKey: .bar) - bar = barDecoded - let recursiveMemberDecoded = try containerValues.decodeIfPresent(RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1.self, forKey: .recursiveMember) - recursiveMember = recursiveMemberDecoded - } - } - """.trimIndent() + val expectedContents = """ +extension RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2: Swift.Codable { + enum CodingKeys: Swift.String, Swift.CodingKey { + case bar + case recursiveMember + } + static func writingClosure(_ value: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested2?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("bar")].write(value.bar) + try writer[.init("recursiveMember")].write(value.recursiveMember, writingClosure: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1.writingClosure(_:to:)) + } + + public init(from decoder: Swift.Decoder) throws { + let containerValues = try decoder.container(keyedBy: CodingKeys.self) + let barDecoded = try containerValues.decodeIfPresent(Swift.String.self, forKey: .bar) + bar = barDecoded + let recursiveMemberDecoded = try containerValues.decodeIfPresent(RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1.self, forKey: .recursiveMember) + recursiveMember = recursiveMemberDecoded + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @Test @@ -92,17 +81,9 @@ class RecursiveShapesEncodeXMLGenerationTests { case nestedRecursiveList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedRecursiveList = nestedRecursiveList { - var nestedRecursiveListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedRecursiveList")) - for nestedrecursiveshapeslist0 in nestedRecursiveList { - var nestedrecursiveshapeslist0Container0 = nestedRecursiveListContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for recursiveshapesinputoutputnested11 in nestedrecursiveshapeslist0 { - try nestedrecursiveshapeslist0Container0.encode(recursiveshapesinputoutputnested11, forKey: ClientRuntime.Key("member")) - } - } - } + static func writingClosure(_ value: XmlNestedRecursiveShapesInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedRecursiveList")].writeList(value.nestedRecursiveList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: RestXmlProtocolClientTypes.RecursiveShapesInputOutputNested1.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/SetEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/SetEncodeXMLGenerationTests.kt index 4ca810d7f..72e20446e 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/SetEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/SetEncodeXMLGenerationTests.kt @@ -25,14 +25,9 @@ class SetEncodeXMLGenerationTests { case fooEnumSet } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let fooEnumSet = fooEnumSet { - var fooEnumSetContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("fooEnumSet")) - for fooenum0 in fooEnumSet { - try fooEnumSetContainer.encode(fooenum0, forKey: ClientRuntime.Key("member")) - } - } + static func writingClosure(_ value: XmlEnumSetInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("fooEnumSet")].writeList(value.fooEnumSet, memberWritingClosure: RestXmlProtocolClientTypes.FooEnum.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() @@ -52,17 +47,9 @@ class SetEncodeXMLGenerationTests { case fooEnumSet } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let fooEnumSet = fooEnumSet { - var fooEnumSetContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("fooEnumSet")) - for fooenumset0 in fooEnumSet { - var fooenumset0Container0 = fooEnumSetContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for fooenum1 in fooenumset0 { - try fooenumset0Container0.encode(fooenum1, forKey: ClientRuntime.Key("member")) - } - } - } + static func writingClosure(_ value: XmlEnumNestedSetInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("fooEnumSet")].writeList(value.fooEnumSet, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: RestXmlProtocolClientTypes.FooEnum.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/StructEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/StructEncodeXMLGenerationTests.kt index 7fe30fb57..b62fffb3e 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/StructEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/StructEncodeXMLGenerationTests.kt @@ -33,38 +33,18 @@ class StructEncodeXMLGenerationTests { case trueBooleanValue } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let byteValue = byteValue { - try container.encode(byteValue, forKey: ClientRuntime.Key("byteValue")) - } - if let doubleValue = doubleValue { - try container.encode(doubleValue, forKey: ClientRuntime.Key("DoubleDribble")) - } - if let falseBooleanValue = falseBooleanValue { - try container.encode(falseBooleanValue, forKey: ClientRuntime.Key("falseBooleanValue")) - } - if let floatValue = floatValue { - try container.encode(floatValue, forKey: ClientRuntime.Key("floatValue")) - } - if let integerValue = integerValue { - try container.encode(integerValue, forKey: ClientRuntime.Key("integerValue")) - } - if let longValue = longValue { - try container.encode(longValue, forKey: ClientRuntime.Key("longValue")) - } - if let `protocol` = `protocol` { - try container.encode(`protocol`, forKey: ClientRuntime.Key("protocol")) - } - if let shortValue = shortValue { - try container.encode(shortValue, forKey: ClientRuntime.Key("shortValue")) - } - if let stringValue = stringValue { - try container.encode(stringValue, forKey: ClientRuntime.Key("stringValue")) - } - if let trueBooleanValue = trueBooleanValue { - try container.encode(trueBooleanValue, forKey: ClientRuntime.Key("trueBooleanValue")) - } + static func writingClosure(_ value: SimpleScalarPropertiesInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("byteValue")].write(value.byteValue) + try writer[.init("DoubleDribble")].write(value.doubleValue) + try writer[.init("falseBooleanValue")].write(value.falseBooleanValue) + try writer[.init("floatValue")].write(value.floatValue) + try writer[.init("integerValue")].write(value.integerValue) + try writer[.init("longValue")].write(value.longValue) + try writer[.init("protocol")].write(value.`protocol`) + try writer[.init("shortValue")].write(value.shortValue) + try writer[.init("stringValue")].write(value.stringValue) + try writer[.init("trueBooleanValue")].write(value.trueBooleanValue) } } """.trimIndent() @@ -83,14 +63,10 @@ class StructEncodeXMLGenerationTests { case b = "other" } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let a = a { - try container.encode(a, forKey: ClientRuntime.Key("value")) - } - if let b = b { - try container.encode(b, forKey: ClientRuntime.Key("other")) - } + static func writingClosure(_ value: RestXmlProtocolClientTypes.StructureListMember?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("value")].write(value.a) + try writer[.init("other")].write(value.b) } public init(from decoder: Swift.Decoder) throws { diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/TimeStampEncodeGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/TimeStampEncodeGenerationTests.kt index d5e82454d..f60920117 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/TimeStampEncodeGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/TimeStampEncodeGenerationTests.kt @@ -27,20 +27,12 @@ class TimeStampEncodeGenerationTests { case normal } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let dateTime = dateTime { - try container.encodeTimestamp(dateTime, format: .dateTime, forKey: ClientRuntime.Key("dateTime")) - } - if let epochSeconds = epochSeconds { - try container.encodeTimestamp(epochSeconds, format: .epochSeconds, forKey: ClientRuntime.Key("epochSeconds")) - } - if let httpDate = httpDate { - try container.encodeTimestamp(httpDate, format: .httpDate, forKey: ClientRuntime.Key("httpDate")) - } - if let normal = normal { - try container.encodeTimestamp(normal, format: .dateTime, forKey: ClientRuntime.Key("normal")) - } + static func writingClosure(_ value: XmlTimestampsInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("dateTime")].writeTimestamp(value.dateTime, format: .dateTime) + try writer[.init("epochSeconds")].writeTimestamp(value.epochSeconds, format: .epochSeconds) + try writer[.init("httpDate")].writeTimestamp(value.httpDate, format: .httpDate) + try writer[.init("normal")].writeTimestamp(value.normal, format: .dateTime) } } """.trimIndent() @@ -59,17 +51,9 @@ class TimeStampEncodeGenerationTests { case nestedTimestampList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedTimestampList = nestedTimestampList { - var nestedTimestampListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedTimestampList")) - for nestedtimestamplist0 in nestedTimestampList { - var nestedtimestamplist0Container0 = nestedTimestampListContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for timestamp1 in nestedtimestamplist0 { - try nestedtimestamplist0Container0.encodeTimestamp(timestamp1, format: .epochSeconds, forKey: ClientRuntime.Key("member")) - } - } - } + static func writingClosure(_ value: XmlTimestampsNestedInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedTimestampList")].writeList(value.nestedTimestampList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: SmithyXML.timestampWritingClosure(format: .epochSeconds), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() @@ -88,17 +72,9 @@ class TimeStampEncodeGenerationTests { case nestedTimestampList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedTimestampList = nestedTimestampList { - var nestedTimestampListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedTimestampList")) - for nestedhttpdatetimestamplist0 in nestedTimestampList { - var nestedhttpdatetimestamplist0Container0 = nestedTimestampListContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("member")) - for timestamp1 in nestedhttpdatetimestamplist0 { - try nestedhttpdatetimestamplist0Container0.encodeTimestamp(timestamp1, format: .httpDate, forKey: ClientRuntime.Key("member")) - } - } - } + static func writingClosure(_ value: XmlTimestampsNestedHTTPDateInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedTimestampList")].writeList(value.nestedTimestampList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: SmithyXML.timestampWritingClosure(format: .httpDate), memberNodeInfo: .init("member"), isFlattened: false), memberNodeInfo: .init("member"), isFlattened: false) } } """.trimIndent() @@ -117,17 +93,9 @@ class TimeStampEncodeGenerationTests { case nestedTimestampList } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let nestedTimestampList = nestedTimestampList { - var nestedTimestampListContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedTimestampList")) - for nestedtimestamplist0 in nestedTimestampList { - var nestedtimestamplist0Container0 = nestedTimestampListContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("nestedTag1")) - for timestamp1 in nestedtimestamplist0 { - try nestedtimestamplist0Container0.encodeTimestamp(timestamp1, format: .epochSeconds, forKey: ClientRuntime.Key("nestedTag2")) - } - } - } + static func writingClosure(_ value: XmlTimestampsNestedXmlNameInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("nestedTimestampList")].writeList(value.nestedTimestampList, memberWritingClosure: SmithyXML.listWritingClosure(memberWritingClosure: SmithyXML.timestampWritingClosure(format: .epochSeconds), memberNodeInfo: .init("nestedTag2"), isFlattened: false), memberNodeInfo: .init("nestedTag1"), isFlattened: false) } } """.trimIndent() @@ -147,14 +115,10 @@ class TimeStampEncodeGenerationTests { case normal = "notNormalName" } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - if let dateTime = dateTime { - try container.encodeTimestamp(dateTime, format: .dateTime, forKey: ClientRuntime.Key("dateTime")) - } - if let normal = normal { - try container.encodeTimestamp(normal, format: .dateTime, forKey: ClientRuntime.Key("notNormalName")) - } + static func writingClosure(_ value: XmlTimestampsXmlNameInput?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + try writer[.init("dateTime")].writeTimestamp(value.dateTime, format: .dateTime) + try writer[.init("notNormalName")].writeTimestamp(value.normal, format: .dateTime) } } """.trimIndent() diff --git a/smithy-swift-codegen/src/test/kotlin/serde/xml/UnionEncodeXMLGenerationTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/xml/UnionEncodeXMLGenerationTests.kt index 485b98eb6..5b6647d4f 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/xml/UnionEncodeXMLGenerationTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/xml/UnionEncodeXMLGenerationTests.kt @@ -17,12 +17,7 @@ class UnionEncodeXMLGenerationTests { fun `001 XmlUnionShape+Codable`() { val context = setupTests("Isolated/Restxml/xml-unions.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/models/XmlUnionShape+Codable.swift") - val expectedContents = - """ -// Code generated by smithy-swift-codegen. DO NOT EDIT! - -import ClientRuntime - + val expectedContents = """ extension RestXmlProtocolClientTypes.XmlUnionShape: Swift.Codable { enum CodingKeys: Swift.String, Swift.CodingKey { case datavalue = "dataValue" @@ -35,35 +30,25 @@ extension RestXmlProtocolClientTypes.XmlUnionShape: Swift.Codable { case unionvalue = "unionValue" } - public func encode(to encoder: Swift.Encoder) throws { - var container = encoder.container(keyedBy: ClientRuntime.Key.self) - switch self { + static func writingClosure(_ value: RestXmlProtocolClientTypes.XmlUnionShape?, to writer: SmithyXML.Writer) throws { + guard let value else { writer.detach(); return } + switch value { case let .datavalue(datavalue): - try container.encode(datavalue, forKey: ClientRuntime.Key("dataValue")) + try writer[.init("dataValue")].write(datavalue) case let .doublevalue(doublevalue): - try container.encode(doublevalue, forKey: ClientRuntime.Key("doubleValue")) + try writer[.init("doubleValue")].write(doublevalue) case let .mapvalue(mapvalue): - var mapValueContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("mapValue")) - for (stringKey0, stringValue0) in mapvalue { - var entryContainer0 = mapValueContainer.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("entry")) - var keyContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("K")) - try keyContainer0.encode(stringKey0, forKey: ClientRuntime.Key("")) - var valueContainer0 = entryContainer0.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("V")) - try valueContainer0.encode(stringValue0, forKey: ClientRuntime.Key("")) - } + try writer[.init("mapValue")].writeMap(mapvalue, valueWritingClosure: Swift.String.writingClosure(_:to:), keyNodeInfo: .init("K"), valueNodeInfo: .init("V"), isFlattened: false) case let .stringlist(stringlist): - var stringlistContainer = container.nestedContainer(keyedBy: ClientRuntime.Key.self, forKey: ClientRuntime.Key("stringList")) - for string0 in stringlist { - try stringlistContainer.encode(string0, forKey: ClientRuntime.Key("member")) - } + try writer[.init("stringList")].writeList(stringlist, memberWritingClosure: Swift.String.writingClosure(_:to:), memberNodeInfo: .init("member"), isFlattened: false) case let .structvalue(structvalue): - try container.encode(structvalue, forKey: ClientRuntime.Key("structValue")) + try writer[.init("structValue")].write(structvalue, writingClosure: RestXmlProtocolClientTypes.XmlNestedUnionStruct.writingClosure(_:to:)) case let .timestampvalue(timestampvalue): - try container.encodeTimestamp(timestampvalue, format: .dateTime, forKey: ClientRuntime.Key("timeStampValue")) + try writer[.init("timeStampValue")].writeTimestamp(timestampvalue, format: .dateTime) case let .unionvalue(unionvalue): - try container.encode(unionvalue, forKey: ClientRuntime.Key("unionValue")) + try writer[.init("unionValue")].write(unionvalue, writingClosure: RestXmlProtocolClientTypes.XmlUnionShape.writingClosure(_:to:)) case let .sdkUnknown(sdkUnknown): - try container.encode(sdkUnknown, forKey: ClientRuntime.Key("sdkUnknown")) + try writer[.init("sdkUnknown")].write(sdkUnknown) } } @@ -123,8 +108,7 @@ extension RestXmlProtocolClientTypes.XmlUnionShape: Swift.Codable { } } } - """.trimIndent() - +""" contents.shouldContainOnlyOnce(expectedContents) } From af13f0027c9148355ff335533e416f19e9c61035 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Thu, 30 Nov 2023 16:44:04 -0600 Subject: [PATCH 23/37] feat!: Use closures for processing HTTP response (#624) --- .../IdempotencyTokenMiddleware.swift | 4 +- .../Middleware/NoopHandler.swift | 9 ++- .../Middleware/OperationStack.swift | 16 ++---- .../Middleware/PresignerShimHandler.swift | 17 ++---- .../Middleware/RetryMiddleware.swift | 13 +++-- .../Middleware/Steps/BuildStep.swift | 14 ++--- .../Middleware/Steps/DeserializeStep.swift | 16 +++--- .../Middleware/Steps/FinalizeStep.swift | 14 ++--- .../Middleware/Steps/InitializeStep.swift | 15 ++--- .../Middleware/Steps/SerializeStep.swift | 15 ++--- .../Networking/Http/HTTPResponseClosure.swift | 16 ++++++ .../Http/HTTPResponseErrorClosure.swift | 17 ++++++ .../Middlewares/ContentLengthMiddleware.swift | 2 +- .../Middlewares/ContentMD5Middleware.swift | 2 +- .../Middlewares/ContentTypeMiddleware.swift | 3 +- .../Middlewares/DeserializeMiddleware.swift | 35 +++++++----- .../Http/Middlewares/HeaderMiddleware.swift | 3 +- .../Http/Middlewares/LoggerMiddleware.swift | 8 +-- .../Middlewares/MutateHeadersMiddleware.swift | 2 +- .../Middlewares/QueryItemMiddleware.swift | 3 +- .../RequestBody/BlobBodyMiddleware.swift | 2 +- .../BlobStreamBodyMiddleware.swift | 2 +- .../RequestBody/BodyMiddleware.swift | 2 +- .../RequestBody/EnumBodyMiddleware.swift | 2 +- .../EventStreamBodyMiddleware.swift | 2 +- .../RequestBody/PayloadBodyMiddleware.swift | 2 +- .../RequestBody/StringBodyMiddleware.swift | 3 +- .../Http/Middlewares/URLHostMiddleware.swift | 3 +- .../Http/Middlewares/URLPathMiddleware.swift | 4 +- .../Networking/Http/SdkHttpClient.swift | 18 +++--- .../Pagination/PaginatorSequence.swift | 30 +++++----- .../MiddlewareTests/OperationStackTests.swift | 2 +- .../MiddlewareTests/ProviderTests.swift | 8 +-- .../MutateHeaderMiddlewareTests.swift | 4 +- .../IdempotencyTokenMiddlewareTests.swift | 2 +- .../MiddlewareStackTests.swift | 8 +-- .../ContentLengthMiddlewareTests.swift | 6 +- .../Retry/RetryIntegrationTests.swift | 4 +- .../HttpRequestTestBaseTests.swift | 2 +- builder.json | 38 ------------- .../swift/codegen/PaginatorGenerator.kt | 2 +- .../HttpProtocolClientGenerator.kt | 1 - .../HttpProtocolUnitTestRequestGenerator.kt | 2 +- .../middlewares/DeserializeMiddleware.kt | 16 +++++- .../middlewares/IdempotencyTokenMiddleware.kt | 3 +- .../middlewares/LoggingMiddleware.kt | 2 +- .../OperationInputUrlPathMiddleware.kt | 3 +- .../middlewares/RetryMiddleware.kt | 5 +- .../MiddlewareExecutionGenerator.kt | 2 +- .../test/kotlin/ContentMd5MiddlewareTests.kt | 13 ++--- .../HttpProtocolClientGeneratorTests.kt | 49 ++++++++--------- ...tpProtocolUnitTestRequestGeneratorTests.kt | 55 +++++++++---------- .../test/kotlin/IdempotencyTokenTraitTests.kt | 13 ++--- .../src/test/kotlin/PaginatorGeneratorTest.kt | 4 +- .../src/test/kotlin/RetryMiddlewareTests.kt | 2 +- 55 files changed, 259 insertions(+), 281 deletions(-) create mode 100644 Sources/ClientRuntime/Networking/Http/HTTPResponseClosure.swift create mode 100644 Sources/ClientRuntime/Networking/Http/HTTPResponseErrorClosure.swift delete mode 100644 builder.json diff --git a/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift b/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift index a8a536121..d46fc5455 100644 --- a/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift +++ b/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift @@ -5,9 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct IdempotencyTokenMiddleware: ClientRuntime.Middleware { +public struct IdempotencyTokenMiddleware: ClientRuntime.Middleware { public let id: Swift.String = "IdempotencyTokenMiddleware" private let keyPath: WritableKeyPath diff --git a/Sources/ClientRuntime/Middleware/NoopHandler.swift b/Sources/ClientRuntime/Middleware/NoopHandler.swift index 065799f5e..b571a7be0 100644 --- a/Sources/ClientRuntime/Middleware/NoopHandler.swift +++ b/Sources/ClientRuntime/Middleware/NoopHandler.swift @@ -5,10 +5,13 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct NoopHandler: Handler { +public struct NoopHandler: Handler { public init() {} - public func handle(context: HttpContext, input: SdkHttpRequest) async throws -> OperationOutput { - return OperationOutput(httpResponse: HttpResponse()) + public func handle( + context: HttpContext, + input: SdkHttpRequest + ) async throws -> OperationOutput { + return OperationOutput(httpResponse: HttpResponse()) } } diff --git a/Sources/ClientRuntime/Middleware/OperationStack.swift b/Sources/ClientRuntime/Middleware/OperationStack.swift index 2e6d4979b..8f0c00dbf 100644 --- a/Sources/ClientRuntime/Middleware/OperationStack.swift +++ b/Sources/ClientRuntime/Middleware/OperationStack.swift @@ -1,9 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0. -public struct OperationStack { +public struct OperationStack { /// returns the unique id for the operation stack as middleware public var id: String @@ -15,14 +13,11 @@ public struct OperationStack(id: InitializeStepId) - self.serializeStep = SerializeStep(id: SerializeStepId) + self.initializeStep = InitializeStep(id: InitializeStepId) + self.serializeStep = SerializeStep(id: SerializeStepId) self.buildStep = BuildStep(id: BuildStepId) self.finalizeStep = FinalizeStep(id: FinalizeStepId) self.deserializeStep = DeserializeStep(id: DeserializeStepId) - } /// This execute will execute the stack and use your next as the last closure in the chain @@ -53,6 +48,7 @@ public struct OperationStack( context: HttpContext, input: OperationStackInput, + output: OperationStackOutput, next: H ) async throws -> SdkHttpRequestBuilder? where H.Input == SdkHttpRequest, @@ -61,9 +57,9 @@ public struct OperationStack(handler: { buildInMiddleware in + middleware: PresignerShim(handler: { buildInMiddleware in builder = buildInMiddleware - })) + }, output: output)) _ = try await handleMiddleware(context: context, input: input, next: next) return builder } diff --git a/Sources/ClientRuntime/Middleware/PresignerShimHandler.swift b/Sources/ClientRuntime/Middleware/PresignerShimHandler.swift index 50236ba73..c8903c64d 100644 --- a/Sources/ClientRuntime/Middleware/PresignerShimHandler.swift +++ b/Sources/ClientRuntime/Middleware/PresignerShimHandler.swift @@ -7,20 +7,20 @@ typealias PresignerShimHandler = (SdkHttpRequestBuilder) -> Void -struct PresignerShim: Middleware { +struct PresignerShim: Middleware { public let id: String = "PresignerShim" private let handler: PresignerShimHandler + private let output: OperationStackOutput - init(handler: @escaping PresignerShimHandler) { + init(handler: @escaping PresignerShimHandler, output: OperationStackOutput) { self.handler = handler + self.output = output } public typealias MInput = SdkHttpRequestBuilder public typealias MOutput = OperationOutput public typealias Context = HttpContext - public typealias MError = OperationStackError public func handle(context: HttpContext, input: SdkHttpRequestBuilder, @@ -31,13 +31,6 @@ struct PresignerShim: Middleware { +public struct RetryMiddleware: Middleware { public typealias MInput = SdkHttpRequestBuilder - public typealias MOutput = OperationOutput + public typealias MOutput = OperationOutput public typealias Context = HttpContext public var id: String { "Retry" } @@ -20,7 +21,8 @@ public struct RetryMiddleware(context: Context, input: SdkHttpRequestBuilder, next: H) async throws -> - OperationOutput where H: Handler, MInput == H.Input, MOutput == H.Output, Context == H.Context { + OperationOutput + where H: Handler, MInput == H.Input, MOutput == H.Output, Context == H.Context { let partitionID = try getPartitionID(context: context, input: input) let token = try await strategy.acquireInitialRetryToken(tokenScope: partitionID) @@ -28,7 +30,8 @@ public struct RetryMiddleware(token: Strategy.Token, context: Context, input: MInput, next: H) async throws -> - OperationOutput where H: Handler, MInput == H.Input, MOutput == H.Output, Context == H.Context { + OperationOutput + where H: Handler, MInput == H.Input, MOutput == H.Output, Context == H.Context { do { let serviceResponse = try await next.handle(context: context, input: input) diff --git a/Sources/ClientRuntime/Middleware/Steps/BuildStep.swift b/Sources/ClientRuntime/Middleware/Steps/BuildStep.swift index 06ec50fb4..30bfb7bae 100644 --- a/Sources/ClientRuntime/Middleware/Steps/BuildStep.swift +++ b/Sources/ClientRuntime/Middleware/Steps/BuildStep.swift @@ -8,16 +8,16 @@ /// Takes Request, and returns result or error. /// /// Receives result or error from Finalize step. -public typealias BuildStep = MiddlewareStep> +public typealias BuildStep = MiddlewareStep> public let BuildStepId = "Build" -public struct BuildStepHandler: Handler where H.Context == HttpContext, - H.Input == SdkHttpRequestBuilder, - H.Output == OperationOutput { +public struct BuildStepHandler: Handler + where H.Context == HttpContext, + H.Input == SdkHttpRequestBuilder, + H.Output == OperationOutput { public typealias Input = SdkHttpRequestBuilder diff --git a/Sources/ClientRuntime/Middleware/Steps/DeserializeStep.swift b/Sources/ClientRuntime/Middleware/Steps/DeserializeStep.swift index e83e028b4..1682028e8 100644 --- a/Sources/ClientRuntime/Middleware/Steps/DeserializeStep.swift +++ b/Sources/ClientRuntime/Middleware/Steps/DeserializeStep.swift @@ -10,16 +10,16 @@ /// Takes Request, and returns result or error. /// /// Receives raw response, or error from underlying handler. -public typealias DeserializeStep = MiddlewareStep> +public typealias DeserializeStep = MiddlewareStep> public let DeserializeStepId = "Deserialize" -public struct DeserializeStepHandler: Handler where H.Context == HttpContext, - H.Input == SdkHttpRequest, - H.Output == OperationOutput { +public struct DeserializeStepHandler: Handler + where H.Context == HttpContext, + H.Input == SdkHttpRequest, + H.Output == OperationOutput { public typealias Input = SdkHttpRequest @@ -36,7 +36,7 @@ public struct DeserializeStepHandler { +public struct OperationOutput { public var httpResponse: HttpResponse public var output: Output? diff --git a/Sources/ClientRuntime/Middleware/Steps/FinalizeStep.swift b/Sources/ClientRuntime/Middleware/Steps/FinalizeStep.swift index 18f9cae04..81132b9c4 100644 --- a/Sources/ClientRuntime/Middleware/Steps/FinalizeStep.swift +++ b/Sources/ClientRuntime/Middleware/Steps/FinalizeStep.swift @@ -9,16 +9,16 @@ // Takes Request, and returns result or error. // // Receives result or error from Deserialize step. -public typealias FinalizeStep = MiddlewareStep> +public typealias FinalizeStep = MiddlewareStep> public let FinalizeStepId = "Finalize" -public struct FinalizeStepHandler: Handler where H.Context == HttpContext, - H.Input == SdkHttpRequest, - H.Output == OperationOutput { +public struct FinalizeStepHandler: Handler + where H.Context == HttpContext, + H.Input == SdkHttpRequest, + H.Output == OperationOutput { public typealias Input = SdkHttpRequestBuilder diff --git a/Sources/ClientRuntime/Middleware/Steps/InitializeStep.swift b/Sources/ClientRuntime/Middleware/Steps/InitializeStep.swift index e859d7b89..a231b3bd4 100644 --- a/Sources/ClientRuntime/Middleware/Steps/InitializeStep.swift +++ b/Sources/ClientRuntime/Middleware/Steps/InitializeStep.swift @@ -7,18 +7,15 @@ /// Takes Input Parameters, and returns result or error. /// /// Receives result or error from Serialize step. -public typealias InitializeStep = MiddlewareStep> +public typealias InitializeStep = + MiddlewareStep> public let InitializeStepId = "Initialize" -public struct InitializeStepHandler: Handler where H.Context == HttpContext, - H.Input == SerializeStepInput, - H.Output == OperationOutput { +public struct InitializeStepHandler: Handler + where H.Context == HttpContext, + H.Input == SerializeStepInput, + H.Output == OperationOutput { public typealias Input = OperationStackInput diff --git a/Sources/ClientRuntime/Middleware/Steps/SerializeStep.swift b/Sources/ClientRuntime/Middleware/Steps/SerializeStep.swift index f1fcb4cf6..01589cecf 100644 --- a/Sources/ClientRuntime/Middleware/Steps/SerializeStep.swift +++ b/Sources/ClientRuntime/Middleware/Steps/SerializeStep.swift @@ -7,18 +7,15 @@ /// Converts Input Parameters into a Request, and returns the result or error. /// /// Receives result or error from Build step. -public typealias SerializeStep = MiddlewareStep, - OperationOutput> +public typealias SerializeStep = + MiddlewareStep, OperationOutput> public let SerializeStepId = "Serialize" -public struct SerializeStepHandler: Handler where H.Context == HttpContext, - H.Input == SdkHttpRequestBuilder, - H.Output == OperationOutput { +public struct SerializeStepHandler: Handler + where H.Context == HttpContext, + H.Input == SdkHttpRequestBuilder, + H.Output == OperationOutput { public typealias Input = SerializeStepInput diff --git a/Sources/ClientRuntime/Networking/Http/HTTPResponseClosure.swift b/Sources/ClientRuntime/Networking/Http/HTTPResponseClosure.swift new file mode 100644 index 000000000..c334d0bb5 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/HTTPResponseClosure.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public typealias HTTPResponseClosure = (HttpResponse) async throws -> OperationStackOutput + +public func responseClosure( + decoder: Decoder +) -> HTTPResponseClosure { + return { response in + try await OperationStackOutput(httpResponse: response, decoder: decoder) + } +} diff --git a/Sources/ClientRuntime/Networking/Http/HTTPResponseErrorClosure.swift b/Sources/ClientRuntime/Networking/Http/HTTPResponseErrorClosure.swift new file mode 100644 index 000000000..65d205e46 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/HTTPResponseErrorClosure.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public typealias HTTPResponseErrorClosure = (HttpResponse) async throws -> Error + +public func responseErrorClosure( + _ errorBinding: OperationErrorBinding.Type, + decoder: Decoder +) -> HTTPResponseErrorClosure { + return { response in + try await OperationErrorBinding.makeError(httpResponse: response, decoder: decoder) + } +} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index ef39978a2..33f5ccbad 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0. -public struct ContentLengthMiddleware: Middleware { +public struct ContentLengthMiddleware: Middleware { public let id: String = "ContentLength" private let contentLengthHeaderName = "Content-Length" diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift index 43264c328..ec61bd458 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift @@ -3,7 +3,7 @@ import AwsCommonRuntimeKit -public struct ContentMD5Middleware: Middleware { +public struct ContentMD5Middleware: Middleware { public let id: String = "ContentMD5" private let contentMD5HeaderName = "Content-MD5" diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentTypeMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentTypeMiddleware.swift index a1274e64f..c8a091e74 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentTypeMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentTypeMiddleware.swift @@ -1,8 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0. -public struct ContentTypeMiddleware: Middleware { +public struct ContentTypeMiddleware: Middleware { public let id: String = "ContentType" diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/DeserializeMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/DeserializeMiddleware.swift index a35b80b9f..84babe99d 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/DeserializeMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/DeserializeMiddleware.swift @@ -1,25 +1,34 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0. - -public struct DeserializeMiddleware: Middleware { +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// +public struct DeserializeMiddleware: Middleware { public var id: String = "Deserialize" - public init() {} + let httpResponseClosure: HTTPResponseClosure + let httpResponseErrorClosure: HTTPResponseErrorClosure + + public init( + _ httpResponseClosure: @escaping HTTPResponseClosure, + _ httpResponseErrorClosure: @escaping HTTPResponseErrorClosure + ) { + self.httpResponseClosure = httpResponseClosure + self.httpResponseErrorClosure = httpResponseErrorClosure + } public func handle(context: HttpContext, input: SdkHttpRequest, - next: H) async throws -> OperationOutput + next: H) async throws -> OperationOutput where H: Handler, Self.MInput == H.Input, Self.MOutput == H.Output, Self.Context == H.Context { - let decoder = context.getDecoder() let response = try await next.handle(context: context, input: input) // call handler to get http response var copiedResponse = response if (200..<300).contains(response.httpResponse.statusCode.rawValue) { - let output = try await Output(httpResponse: copiedResponse.httpResponse, - decoder: decoder) + let output = try await httpResponseClosure(copiedResponse.httpResponse) copiedResponse.output = output return copiedResponse } else { @@ -29,14 +38,12 @@ public struct DeserializeMiddleware + public typealias MOutput = OperationOutput public typealias Context = HttpContext } diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/HeaderMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/HeaderMiddleware.swift index 1685e9974..db2393a41 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/HeaderMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/HeaderMiddleware.swift @@ -5,8 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct HeaderMiddleware: Middleware { +public struct HeaderMiddleware: Middleware { public let id: String = "\(String(describing: OperationStackInput.self))HeadersMiddleware" public init() {} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/LoggerMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/LoggerMiddleware.swift index f2c430289..31030d334 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/LoggerMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/LoggerMiddleware.swift @@ -5,8 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct LoggerMiddleware: Middleware { +public struct LoggerMiddleware: Middleware { public let id: String = "Logger" @@ -18,7 +17,7 @@ public struct LoggerMiddleware(context: Context, input: SdkHttpRequest, - next: H) async throws -> OperationOutput + next: H) async throws -> OperationOutput where H: Handler, Self.MInput == H.Input, Self.MOutput == H.Output, @@ -46,7 +45,6 @@ public struct LoggerMiddleware + public typealias MOutput = OperationOutput public typealias Context = HttpContext - public typealias MError = OutputError } diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/MutateHeadersMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/MutateHeadersMiddleware.swift index baeca0cb3..8266f0fe5 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/MutateHeadersMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/MutateHeadersMiddleware.swift @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0. -public struct MutateHeadersMiddleware: Middleware { +public struct MutateHeadersMiddleware: Middleware { public let id: String = "MutateHeaders" diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/QueryItemMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/QueryItemMiddleware.swift index 3693bbf99..a8370152e 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/QueryItemMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/QueryItemMiddleware.swift @@ -5,8 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct QueryItemMiddleware: Middleware { +public struct QueryItemMiddleware: Middleware { public let id: String = "\(String(describing: OperationStackInput.self))QueryItemMiddleware" public init() {} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift index 3d0606e7a..b046349c1 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift @@ -8,7 +8,7 @@ import struct Foundation.Data public struct BlobBodyMiddleware: Middleware { + OperationStackOutput>: Middleware { public let id: Swift.String = "BlobBodyMiddleware" let keyPath: KeyPath diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobStreamBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobStreamBodyMiddleware.swift index 16b94a7b0..2da8cde29 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobStreamBodyMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobStreamBodyMiddleware.swift @@ -8,7 +8,7 @@ import struct Foundation.Data public struct BlobStreamBodyMiddleware: Middleware { + OperationStackOutput>: Middleware { public let id: Swift.String = "BlobStreamBodyMiddleware" let keyPath: KeyPath diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BodyMiddleware.swift index 208cb29af..1e0e0972f 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BodyMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BodyMiddleware.swift @@ -10,7 +10,7 @@ import typealias SmithyReadWrite.DocumentWritingClosure import typealias SmithyReadWrite.WritingClosure public struct BodyMiddleware: Middleware { public let id: Swift.String = "BodyMiddleware" diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EnumBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EnumBodyMiddleware.swift index ed5870b1f..20dbbe48e 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EnumBodyMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EnumBodyMiddleware.swift @@ -8,7 +8,7 @@ import struct Foundation.Data public struct EnumBodyMiddleware: Middleware where Primitive.RawValue == String { public let id: Swift.String = "EnumBodyMiddleware" diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EventStreamBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EventStreamBodyMiddleware.swift index f92ba807f..0e7278b25 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EventStreamBodyMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EventStreamBodyMiddleware.swift @@ -10,7 +10,7 @@ import typealias SmithyReadWrite.DocumentWritingClosure import typealias SmithyReadWrite.WritingClosure public struct EventStreamBodyMiddleware: Middleware { public let id: Swift.String = "EventStreamBodyMiddleware" diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/PayloadBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/PayloadBodyMiddleware.swift index 04e15cbb1..e2c8e0e80 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/PayloadBodyMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/PayloadBodyMiddleware.swift @@ -10,7 +10,7 @@ import typealias SmithyReadWrite.DocumentWritingClosure import typealias SmithyReadWrite.WritingClosure public struct PayloadBodyMiddleware: Middleware { public let id: Swift.String = "PayloadBodyMiddleware" diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/StringBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/StringBodyMiddleware.swift index cacbfe39c..b68a7f817 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/StringBodyMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/StringBodyMiddleware.swift @@ -7,8 +7,7 @@ import struct Foundation.Data -public struct StringBodyMiddleware: Middleware { +public struct StringBodyMiddleware: Middleware { public let id: Swift.String = "\(OperationStackInput.self)StringBodyMiddleware" let keyPath: KeyPath diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/URLHostMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/URLHostMiddleware.swift index a7f02f89a..f5339df2f 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/URLHostMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/URLHostMiddleware.swift @@ -5,8 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct URLHostMiddleware: Middleware { +public struct URLHostMiddleware: Middleware { public let id: String = "\(String(describing: OperationStackInput.self))URLHostMiddleware" let host: String? diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/URLPathMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/URLPathMiddleware.swift index e5dbad7d8..b90ea0596 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/URLPathMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/URLPathMiddleware.swift @@ -5,9 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct URLPathMiddleware: ClientRuntime.Middleware { +public struct URLPathMiddleware: Middleware { public let id: Swift.String = "\(String(describing: OperationStackInput.self))URLPathMiddleware" let urlPrefix: Swift.String? diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift index 080ca357c..95ad0d55f 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift @@ -12,10 +12,10 @@ public class SdkHttpClient { self.engine = engine } - public func getHandler() -> AnyHandler, - HttpContext> { - let clientHandler = ClientHandler(engine: engine) + public func getHandler() + -> AnyHandler, HttpContext> { + + let clientHandler = ClientHandler(engine: engine) return clientHandler.eraseToAnyHandler() } @@ -24,9 +24,9 @@ public class SdkHttpClient { } } -struct ClientHandler: Handler { +struct ClientHandler: Handler { let engine: HttpClientEngine - func handle(context: HttpContext, input: SdkHttpRequest) async throws -> OperationOutput { + func handle(context: HttpContext, input: SdkHttpRequest) async throws -> OperationOutput { let httpResponse: HttpResponse if context.shouldForceH2(), let crtEngine = engine as? CRTClientEngine { @@ -35,12 +35,10 @@ struct ClientHandler: Handler { httpResponse = try await engine.execute(request: input) } - return OperationOutput(httpResponse: httpResponse) + return OperationOutput(httpResponse: httpResponse) } typealias Input = SdkHttpRequest - - typealias Output = OperationOutput - + typealias Output = OperationOutput typealias Context = HttpContext } diff --git a/Sources/ClientRuntime/Pagination/PaginatorSequence.swift b/Sources/ClientRuntime/Pagination/PaginatorSequence.swift index 7e5d4815f..6006573e6 100644 --- a/Sources/ClientRuntime/Pagination/PaginatorSequence.swift +++ b/Sources/ClientRuntime/Pagination/PaginatorSequence.swift @@ -5,19 +5,19 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct PaginatorSequence: AsyncSequence -where Input.Token: Equatable { - public typealias Element = Output - let input: Input - let inputKey: KeyPath? - let outputKey: KeyPath - let paginationFunction: (Input) async throws -> Output +public struct PaginatorSequence: AsyncSequence + where OperationStackInput.Token: Equatable { - public init(input: Input, - inputKey: KeyPath? = nil, - outputKey: KeyPath, - paginationFunction: @escaping (Input) async throws -> Output) { + public typealias Element = OperationStackOutput + let input: OperationStackInput + let inputKey: KeyPath? + let outputKey: KeyPath + let paginationFunction: (OperationStackInput) async throws -> OperationStackOutput + + public init(input: OperationStackInput, + inputKey: KeyPath? = nil, + outputKey: KeyPath, + paginationFunction: @escaping (OperationStackInput) async throws -> OperationStackOutput) { self.input = input self.inputKey = inputKey self.outputKey = outputKey @@ -25,13 +25,13 @@ where Input.Token: Equatable { } public struct PaginationIterator: AsyncIteratorProtocol { - var input: Input + var input: OperationStackInput let sequence: PaginatorSequence - var token: Input.Token? + var token: OperationStackInput.Token? var isFirstPage: Bool = true // swiftlint:disable force_cast - public mutating func next() async throws -> Output? { + public mutating func next() async throws -> OperationStackOutput? { while token != nil || isFirstPage { if let token = token, diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift index 661537eef..6582b211a 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift @@ -22,7 +22,7 @@ class OperationStackTests: HttpRequestTestBase { .withOperation(value: "Test Operation") let builtContext = addContextValues.build() - var stack = OperationStack(id: "Test Operation") + var stack = OperationStack(id: "Test Operation") stack.initializeStep.intercept(position: .before, middleware: MockInitializeMiddleware(id: "TestInitializeMiddleware", callback: { _, _ in self.checkOrder(&currExpectCount, 1) })) diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift index bc910d160..4fdc323f6 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift @@ -24,8 +24,8 @@ class ProviderTests: HttpRequestTestBase { let context = HttpContextBuilder().withDecoder(value: JSONDecoder()).build() - var operationStack = OperationStack(id: "testURLPathOperation") - operationStack.initializeStep.intercept(position: .after, middleware: URLPathMiddleware()) + var operationStack = OperationStack(id: "testURLPathOperation") + operationStack.initializeStep.intercept(position: .after, middleware: URLPathMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware(id: "TestDeserializeMiddleware")) _ = try await operationStack.handleMiddleware(context: context, input: mockInput, @@ -44,7 +44,7 @@ class ProviderTests: HttpRequestTestBase { let context = HttpContextBuilder().withDecoder(value: JSONDecoder()).build() - var operationStack = OperationStack(id: "testURLPathOperation") + var operationStack = OperationStack(id: "testURLPathOperation") operationStack.serializeStep.intercept(position: .after, middleware: QueryItemMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware(id: "TestDeserializeMiddleware")) _ = try await operationStack.handleMiddleware(context: context, @@ -74,7 +74,7 @@ class ProviderTests: HttpRequestTestBase { let context = HttpContextBuilder().withDecoder(value: JSONDecoder()).build() - var operationStack = OperationStack(id: "testURLPathOperation") + var operationStack = OperationStack(id: "testURLPathOperation") operationStack.serializeStep.intercept(position: .after, middleware: HeaderMiddleware()) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware(id: "TestDeserializeMiddleware")) _ = try await operationStack.handleMiddleware(context: context, diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/MutateHeaderMiddlewareTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/MutateHeaderMiddlewareTests.swift index cad74db18..988cc8497 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/MutateHeaderMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/MutateHeaderMiddlewareTests.swift @@ -14,7 +14,7 @@ class MutateHeaderMiddlewareTests: XCTestCase { var clientEngine: MockHttpClientEngine! = nil var httpClient: SdkHttpClient! = nil var builtContext: HttpContext! = nil - var stack: OperationStack! = nil + var stack: OperationStack! = nil override func setUp() { httpClientConfiguration = HttpClientConfiguration() clientEngine = MockHttpClientEngine() @@ -26,7 +26,7 @@ class MutateHeaderMiddlewareTests: XCTestCase { .withDecoder(value: JSONDecoder()) .withOperation(value: "Test Operation") .build() - stack = OperationStack(id: "Test Operation") + stack = OperationStack(id: "Test Operation") stack.serializeStep.intercept(position: .after, middleware: MockSerializeMiddleware(id: "TestMiddleware", headerName: "TestName", headerValue: "TestValue")) stack.deserializeStep.intercept(position: .after, diff --git a/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift b/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift index 1728c396e..d5b88fb62 100644 --- a/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift @@ -11,7 +11,7 @@ import XCTest class IdempotencyTokenMiddlewareTests: XCTestCase { - private typealias Subject = IdempotencyTokenMiddleware + private typealias Subject = IdempotencyTokenMiddleware let token = "def" let previousToken = "abc" diff --git a/Tests/ClientRuntimeTests/MiddlewareTests/MiddlewareStackTests.swift b/Tests/ClientRuntimeTests/MiddlewareTests/MiddlewareStackTests.swift index 5b0afe030..f29a33eb0 100644 --- a/Tests/ClientRuntimeTests/MiddlewareTests/MiddlewareStackTests.swift +++ b/Tests/ClientRuntimeTests/MiddlewareTests/MiddlewareStackTests.swift @@ -14,7 +14,7 @@ class MiddlewareStackTests: XCTestCase { .withDecoder(value: JSONDecoder()) .withOperation(value: "Test Operation") .build() - var stack = OperationStack(id: "Test Operation") + var stack = OperationStack(id: "Test Operation") stack.serializeStep.intercept(position: .after, middleware: MockSerializeMiddleware(id: "TestMiddleware", headerName: "TestHeaderName1", headerValue: "TestHeaderValue1")) stack.deserializeStep.intercept(position: .after, @@ -40,7 +40,7 @@ class MiddlewareStackTests: XCTestCase { .withDecoder(value: JSONDecoder()) .withOperation(value: "Test Operation") .build() - var stack = OperationStack(id: "Test Operation") + var stack = OperationStack(id: "Test Operation") stack.initializeStep.intercept(position: .before, id: "create http request") { (context, input, next) -> OperationOutput in return try await next.handle(context: context, input: input) @@ -57,7 +57,7 @@ class MiddlewareStackTests: XCTestCase { return try await next.handle(context: context, input: requestBuilder) } stack.finalizeStep.intercept(position: .before, middleware: ContentLengthMiddleware()) - stack.deserializeStep.intercept(position: .after, middleware: DeserializeMiddleware()) + stack.deserializeStep.intercept(position: .after, middleware: DeserializeMiddleware(responseClosure(decoder: JSONDecoder()), responseErrorClosure(MockMiddlewareError.self, decoder: JSONDecoder()))) let result = try await stack.handleMiddleware(context: builtContext, input: MockInput(), next: MockHandler(handleCallback: { (_, input) in XCTAssert(input.headers.value(for: "TestHeaderName2") == "TestHeaderValue2") @@ -86,7 +86,7 @@ class MiddlewareStackTests: XCTestCase { .withDecoder(value: JSONDecoder()) .withOperation(value: "Test Operation") .build() - var stack = OperationStack(id: "Test Operation") + var stack = OperationStack(id: "Test Operation") stack.serializeStep.intercept(position: .after, middleware: MockSerializeMiddleware(id: "TestMiddleware", headerName: "TestName", headerValue: "TestValue")) stack.deserializeStep.intercept(position: .after, diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift index e2b48fe23..e63e42dd0 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -7,7 +7,7 @@ import SmithyTestUtil class ContentLengthMiddlewareTests: XCTestCase { private var builtContext: HttpContext! - private var stack: OperationStack! + private var stack: OperationStack! override func setUpWithError() throws { try super.setUpWithError() @@ -18,7 +18,7 @@ class ContentLengthMiddlewareTests: XCTestCase { .withDecoder(value: JSONDecoder()) .withOperation(value: "Test Operation") .build() - stack = OperationStack(id: "Test Operation") + stack = OperationStack(id: "Test Operation") } func testTransferEncodingChunkedSetWhenStreamLengthIsNil() async throws { @@ -89,4 +89,4 @@ class ContentLengthMiddlewareTests: XCTestCase { _ = try await stack.handleMiddleware(context: builtContext, input: MockInput(), next: mockHandler) } -} \ No newline at end of file +} diff --git a/Tests/ClientRuntimeTests/Retry/RetryIntegrationTests.swift b/Tests/ClientRuntimeTests/Retry/RetryIntegrationTests.swift index 87c0a092e..1f45c2a6d 100644 --- a/Tests/ClientRuntimeTests/Retry/RetryIntegrationTests.swift +++ b/Tests/ClientRuntimeTests/Retry/RetryIntegrationTests.swift @@ -16,7 +16,7 @@ final class RetryIntegrationTests: XCTestCase { private var context: HttpContext! private var next: TestOutputHandler! - private var subject: RetryMiddleware! + private var subject: RetryMiddleware! private var quota: RetryQuota { get async { await subject.strategy.quotaRepository.quota(partitionID: partitionID) } } private func setUp(availableCapacity: Int, maxCapacity: Int, maxRetriesBase: Int, maxBackoff: TimeInterval) async { @@ -34,7 +34,7 @@ final class RetryIntegrationTests: XCTestCase { // Create a retry strategy with custom backoff strategy & custom max retries & custom capacity let retryStrategyOptions = RetryStrategyOptions(backoffStrategy: backoffStrategy, maxRetriesBase: maxRetriesBase, availableCapacity: availableCapacity, maxCapacity: maxCapacity) - subject = RetryMiddleware(options: retryStrategyOptions) + subject = RetryMiddleware(options: retryStrategyOptions) // Replace the retry strategy's sleeper with a mock, to allow tests to run without delay and for us to // check the delay time diff --git a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift index 65816ebee..130c315c6 100644 --- a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift +++ b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift @@ -170,7 +170,7 @@ class HttpRequestTestBaseTests: HttpRequestTestBase { forbiddenHeader: "forbidden header", requiredHeader: "required header") - var operationStack = OperationStack(id: "SayHelloInputRequest") + var operationStack = OperationStack(id: "SayHelloInputRequest") operationStack.initializeStep.intercept(position: .before, middleware: SayHelloInputURLHostMiddleware(host: HttpRequestTestBaseTests.host)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) diff --git a/builder.json b/builder.json deleted file mode 100644 index 690e974ca..000000000 --- a/builder.json +++ /dev/null @@ -1,38 +0,0 @@ - -{ - "name": "smithy-swift", - "needs_compiler": false, - "packages": [], - "variables": { - "gradlew": "{source_dir}/gradlew -p {source_dir}" - }, - "test_steps": [ - "cd {source_dir} && swift test" - ], - "build_steps": [ - "{gradlew} build", - "cd {source_dir} && swift build" - ], - "post_build_steps": [ - "{gradlew} publishToMavenLocal" - ], - "build_dir": "target/build", - "downstream": [ - { "name": "aws-sdk-swift" } - ], - "upstream": [ - { "name": "aws-crt-swift" } - ], - "hosts": { - "ubuntu": { - "packages": [ - "openjdk-8-jdk-headless" - ] - }, - "debian": { - "packages": [ - "openjdk-8-jdk-headless" - ] - } - } -} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PaginatorGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PaginatorGenerator.kt index 3ca342f99..ddc6bf355 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PaginatorGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/PaginatorGenerator.kt @@ -182,7 +182,7 @@ class PaginatorGenerator : SwiftIntegration { writer.write("") val itemSymbolShape = itemDesc.itemSymbol.getProperty("shape").getOrNull() as? Shape - writer.openBlock("extension PaginatorSequence where Input == \$N, Output == \$N {", "}", inputSymbol, outputSymbol) { + writer.openBlock("extension PaginatorSequence where OperationStackInput == \$N, OperationStackOutput == \$N {", "}", inputSymbol, outputSymbol) { val docBody = """ This paginator transforms the `AsyncSequence` returned by `${operationShape.toLowerCamelCase()}Paginated` to access the nested member `${itemDesc.collectionLiteral}` diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt index e66453dbb..8f0d9b311 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt @@ -53,7 +53,6 @@ open class HttpProtocolClientGenerator( ServiceGenerator.renderOperationDefinition(model, serviceShape, symbolProvider, writer, operationsIndex, it) writer.openBlock("{", "}") { val operationStackName = "operation" - writer.write("let encoder = self.encoder") val generator = MiddlewareExecutionGenerator(ctx, writer, httpBindingResolver, httpProtocolCustomizable, operationMiddleware, operationStackName) generator.render(serviceShape, it) { writer, labelMemberName -> writer.write("throw \$N(\"uri component $labelMemberName unexpectedly nil\")", ClientRuntimeTypes.Core.UnknownClientError) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt index 5dd482384..8d3166850 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt @@ -106,7 +106,7 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B writer.write(" .build()") } val operationStack = "operationStack" - writer.write("var $operationStack = OperationStack<$inputSymbol, $outputSymbol, $outputErrorName>(id: \"${test.id}\")") + writer.write("var $operationStack = OperationStack<$inputSymbol, $outputSymbol>(id: \"${test.id}\")") operationMiddleware.renderMiddleware(ctx, writer, operation, operationStack, MiddlewareStep.INITIALIZESTEP) operationMiddleware.renderMiddleware(ctx, writer, operation, operationStack, MiddlewareStep.BUILDSTEP) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/DeserializeMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/DeserializeMiddleware.kt index 32d030d06..427c13592 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/DeserializeMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/DeserializeMiddleware.kt @@ -30,6 +30,20 @@ class DeserializeMiddleware( ) { val output = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op) val outputError = MiddlewareShapeUtils.outputErrorSymbol(op) - writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<\$N, \$N>())", ClientRuntimeTypes.Middleware.DeserializeMiddleware, output, outputError) + val httpResponseClosure = "responseClosure(decoder: decoder)" + val httpResponseErrorClosure = writer.format( + "responseErrorClosure(\$N.self, decoder: decoder)", + outputError + ) + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$N>(\$L, \$L))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.DeserializeMiddleware, + output, + httpResponseClosure, + httpResponseErrorClosure + ) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt index c54442e2d..8e2e30ee0 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt @@ -35,14 +35,13 @@ class IdempotencyTokenMiddleware( val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name val outputErrorShapeName = MiddlewareShapeUtils.outputErrorSymbolName(op) writer.write( - "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L, \$L>(keyPath: \\.\$L))", + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L>(keyPath: \\.\$L))", operationStackName, middlewareStep.stringValue(), position.stringValue(), ClientRuntimeTypes.Middleware.IdempotencyTokenMiddleware, inputShapeName, outputShapeName, - outputErrorShapeName, idempotentMemberName ) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/LoggingMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/LoggingMiddleware.kt index 6b4bf248d..f9a475460 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/LoggingMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/LoggingMiddleware.kt @@ -35,7 +35,7 @@ class LoggingMiddleware( ) { val output = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op) val outputError = MiddlewareShapeUtils.outputErrorSymbol(op) - writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<\$N, \$N>(${middlewareParamsString()}))", ClientRuntimeTypes.Middleware.LoggerMiddleware, output, outputError) + writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<\$N>(${middlewareParamsString()}))", ClientRuntimeTypes.Middleware.LoggerMiddleware, output) } private fun middlewareParamsString(): String { diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt index e5109b0de..c94c46eff 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt @@ -31,7 +31,6 @@ class OperationInputUrlPathMiddleware( ) { val inputShapeName = MiddlewareShapeUtils.inputSymbol(symbolProvider, model, op).name val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name - val errorShapeName = MiddlewareShapeUtils.outputErrorSymbolName(op) - writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<$inputShapeName, $outputShapeName, $errorShapeName>($inputParameters))", ClientRuntimeTypes.Middleware.URLPathMiddleware) + writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<$inputShapeName, $outputShapeName>($inputParameters))", ClientRuntimeTypes.Middleware.URLPathMiddleware) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RetryMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RetryMiddleware.kt index d9b28a28d..3cb9489aa 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RetryMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/RetryMiddleware.kt @@ -33,12 +33,11 @@ class RetryMiddleware( val output = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op) val outputError = MiddlewareShapeUtils.outputErrorSymbol(op) writer.write( - "$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<\$N, \$N, \$N, \$N>(options: config.retryStrategyOptions))", + "$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<\$N, \$N, \$N>(options: config.retryStrategyOptions))", ClientRuntimeTypes.Middleware.RetryMiddleware, ClientRuntimeTypes.Core.DefaultRetryStrategy, retryErrorInfoProviderSymbol, - output, - outputError + output ) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt index caed48741..f980b2066 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt @@ -36,7 +36,7 @@ class MiddlewareExecutionGenerator( renderContextAttributes(op) } httpProtocolCustomizable.renderEventStreamAttributes(ctx, writer, op) - writer.write("var $operationStackName = \$N<$inputShapeName, $outputShapeName, $operationErrorName>(id: \"${op.toLowerCamelCase()}\")", OperationStack) + writer.write("var $operationStackName = \$N<$inputShapeName, $outputShapeName>(id: \"${op.toLowerCamelCase()}\")", OperationStack) renderMiddlewares(ctx, op, operationStackName) } diff --git a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt index 10b97b3ce..629e2bf90 100644 --- a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt @@ -9,7 +9,6 @@ class ContentMd5MiddlewareTests { val expectedContents = """ public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput { - let encoder = self.encoder let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -20,17 +19,17 @@ class ContentMd5MiddlewareTests { .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) .build() - var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.ContentMD5Middleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: SmithyXML.XMLReadWrite.documentWritingClosure(rootNodeInfo: .init("IdempotencyToken")), inputWritingClosure: IdempotencyTokenWithStructureInput.writingClosure(_:to:))) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware(responseClosure(decoder: decoder), responseErrorClosure(IdempotencyTokenWithStructureOutputError.self, decoder: decoder))) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) return result } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index 89ec7674f..3a7a0e886 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -108,7 +108,6 @@ class HttpProtocolClientGeneratorTests { val expected = """ public func allocateWidget(input: AllocateWidgetInput) async throws -> AllocateWidgetOutput { - let encoder = self.encoder let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -119,16 +118,16 @@ class HttpProtocolClientGeneratorTests { .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) .build() - var operation = ClientRuntime.OperationStack(id: "allocateWidget") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.clientToken)) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + var operation = ClientRuntime.OperationStack(id: "allocateWidget") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.clientToken)) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware(responseClosure(decoder: decoder), responseErrorClosure(AllocateWidgetOutputError.self, decoder: decoder))) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) return result } @@ -141,11 +140,9 @@ class HttpProtocolClientGeneratorTests { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() - val expected = - """ + val expected = """ public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput { - let encoder = self.encoder let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -156,15 +153,15 @@ class HttpProtocolClientGeneratorTests { .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: false, unsignedPayload: true)) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware(responseClosure(decoder: decoder), responseErrorClosure(UnsignedFooBlobStreamOutputError.self, decoder: decoder))) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) return result } @@ -180,7 +177,6 @@ class HttpProtocolClientGeneratorTests { val expected = """ public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput { - let encoder = self.encoder let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -191,15 +187,15 @@ class HttpProtocolClientGeneratorTests { .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BlobStreamBodyMiddleware(keyPath: \.payload1)) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware(responseClosure(decoder: decoder), responseErrorClosure(UnsignedFooBlobStreamWithLengthOutputError.self, decoder: decoder))) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) return result } @@ -215,7 +211,6 @@ class HttpProtocolClientGeneratorTests { val expected = """ public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput { - let encoder = self.encoder let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -226,15 +221,15 @@ class HttpProtocolClientGeneratorTests { .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BlobStreamBodyMiddleware(keyPath: \.payload1)) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware(requiresLength: true, unsignedPayload: true)) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware(responseClosure(decoder: decoder), responseErrorClosure(UnsignedFooBlobStreamWithLengthOutputError.self, decoder: decoder))) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) return result } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt index 7f4a9c42b..39e8e20c7 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt @@ -27,8 +27,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { val contents = getTestFileContents("example", "SmokeTestRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testSmokeTest() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -82,8 +81,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .post) .build() - var operationStack = OperationStack(id: "SmokeTest") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "SmokeTest") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -170,8 +169,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .post) .build() - var operationStack = OperationStack(id: "ExplicitString") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "ExplicitString") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -239,8 +238,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .post) .build() - var operationStack = OperationStack(id: "RestJsonEmptyInputAndEmptyOutput") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "RestJsonEmptyInputAndEmptyOutput") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -305,8 +304,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .put) .build() - var operationStack = OperationStack(id: "RestJsonDoesntSerializeNullStructureValues") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "RestJsonDoesntSerializeNullStructureValues") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -363,8 +362,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { fun `it creates a unit test with a string to be converted to data`() { val contents = getTestFileContents("example", "StreamingTraitsRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testRestJsonStreamingTraitsWithBlob() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -397,8 +395,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .post) .build() - var operationStack = OperationStack(id: "RestJsonStreamingTraitsWithBlob") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "RestJsonStreamingTraitsWithBlob") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -473,8 +471,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .get) .build() - var operationStack = OperationStack(id: "RestJsonHttpPrefixHeadersAreNotPresent") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "RestJsonHttpPrefixHeadersAreNotPresent") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -508,8 +506,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { fun `it creates a unit test for union shapes`() { val contents = getTestFileContents("example", "JsonUnionsRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testRestJsonSerializeStringUnionValue() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -545,8 +542,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .put) .build() - var operationStack = OperationStack(id: "RestJsonSerializeStringUnionValue") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "RestJsonSerializeStringUnionValue") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -649,8 +646,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .put) .build() - var operationStack = OperationStack(id: "RestJsonRecursiveShapes") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "RestJsonRecursiveShapes") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -698,8 +695,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { fun `it creates a unit test for inline document`() { val contents = getTestFileContents("example", "InlineDocumentRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testInlineDocumentInput() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -742,8 +738,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .put) .build() - var operationStack = OperationStack(id: "InlineDocumentInput") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "InlineDocumentInput") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -784,7 +780,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { throw serviceError }) } - """ + } +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -831,8 +828,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withEncoder(value: encoder) .withMethod(value: .put) .build() - var operationStack = OperationStack(id: "InlineDocumentAsPayloadInput") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + var operationStack = OperationStack(id: "InlineDocumentAsPayloadInput") + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) diff --git a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt index f99f6e185..a0802641d 100644 --- a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt @@ -9,7 +9,6 @@ class IdempotencyTokenTraitTests { val expectedContents = """ public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput { - let encoder = self.encoder let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -20,16 +19,16 @@ class IdempotencyTokenTraitTests { .withLogger(value: config.logger) .withPartitionID(value: config.partitionID) .build() - var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml")) operation.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: SmithyXML.XMLReadWrite.documentWritingClosure(rootNodeInfo: .init("IdempotencyToken")), inputWritingClosure: IdempotencyTokenWithStructureInput.writingClosure(_:to:))) operation.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware()) - operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.DeserializeMiddleware(responseClosure(decoder: decoder), responseErrorClosure(IdempotencyTokenWithStructureOutputError.self, decoder: decoder))) + operation.deserializeStep.intercept(position: .after, middleware: ClientRuntime.LoggerMiddleware(clientLogMode: config.clientLogMode)) let result = try await operation.handleMiddleware(context: context, input: input, next: client.getHandler()) return result } diff --git a/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt b/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt index a8cea099d..b2c611894 100644 --- a/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt +++ b/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt @@ -72,7 +72,7 @@ class PaginatorGeneratorTest { )} } - extension PaginatorSequence where Input == ListFunctionsInput, Output == ListFunctionsOutput { + extension PaginatorSequence where OperationStackInput == ListFunctionsInput, OperationStackOutput == ListFunctionsOutput { /// This paginator transforms the `AsyncSequence` returned by `listFunctionsPaginated` /// to access the nested member `[TestClientTypes.FunctionConfiguration]` /// - Returns: `[TestClientTypes.FunctionConfiguration]` @@ -112,7 +112,7 @@ class PaginatorGeneratorTest { )} } - extension PaginatorSequence where Input == PaginatedMapInput, Output == PaginatedMapOutput { + extension PaginatorSequence where OperationStackInput == PaginatedMapInput, OperationStackOutput == PaginatedMapOutput { /// This paginator transforms the `AsyncSequence` returned by `paginatedMapPaginated` /// to access the nested member `[(String, Swift.Int)]` /// - Returns: `[(String, Swift.Int)]` diff --git a/smithy-swift-codegen/src/test/kotlin/RetryMiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/RetryMiddlewareTests.kt index ec5823bfa..95f6d17be 100644 --- a/smithy-swift-codegen/src/test/kotlin/RetryMiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/RetryMiddlewareTests.kt @@ -8,7 +8,7 @@ class RetryMiddlewareTests { val context = setupTests("Isolated/contentmd5checksum.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/RestXmlProtocolClient.swift") val expectedContents = """ - operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) + operation.finalizeStep.intercept(position: .after, middleware: ClientRuntime.RetryMiddleware(options: config.retryStrategyOptions)) """.trimIndent() contents.shouldContainOnlyOnce(expectedContents) } From 794b36b2d3dda9ed0e5d4ef0ac5ece3f96248f16 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 1 Dec 2023 12:30:51 -0800 Subject: [PATCH 24/37] feat: add custom trait PaginationTruncationMember (#625) --- .../Middleware/Steps/DeserializeStep.swift | 2 +- .../Networking/Http/SdkHttpClient.swift | 2 +- .../Pagination/PaginatorSequence.swift | 13 +++++ .../swift/codegen/PaginatorGenerator.kt | 33 +++++++----- .../PaginationTruncationMember.kt | 23 +++++++++ .../src/test/kotlin/PaginatorGeneratorTest.kt | 18 +++++-- .../resources/pagination-truncation.smithy | 50 +++++++++++++++++++ 7 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/customtraits/PaginationTruncationMember.kt create mode 100644 smithy-swift-codegen/src/test/resources/pagination-truncation.smithy diff --git a/Sources/ClientRuntime/Middleware/Steps/DeserializeStep.swift b/Sources/ClientRuntime/Middleware/Steps/DeserializeStep.swift index 1682028e8..2dcf50f9f 100644 --- a/Sources/ClientRuntime/Middleware/Steps/DeserializeStep.swift +++ b/Sources/ClientRuntime/Middleware/Steps/DeserializeStep.swift @@ -16,7 +16,7 @@ public typealias DeserializeStep = MiddlewareStep: Handler +public struct DeserializeStepHandler: Handler where H.Context == HttpContext, H.Input == SdkHttpRequest, H.Output == OperationOutput { diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift index 95ad0d55f..b1dfa735b 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift @@ -12,7 +12,7 @@ public class SdkHttpClient { self.engine = engine } - public func getHandler() + public func getHandler() -> AnyHandler, HttpContext> { let clientHandler = ClientHandler(engine: engine) diff --git a/Sources/ClientRuntime/Pagination/PaginatorSequence.swift b/Sources/ClientRuntime/Pagination/PaginatorSequence.swift index 6006573e6..5ca294378 100644 --- a/Sources/ClientRuntime/Pagination/PaginatorSequence.swift +++ b/Sources/ClientRuntime/Pagination/PaginatorSequence.swift @@ -12,15 +12,18 @@ public struct PaginatorSequence? let outputKey: KeyPath + var isTruncatedKey: KeyPath? let paginationFunction: (OperationStackInput) async throws -> OperationStackOutput public init(input: OperationStackInput, inputKey: KeyPath? = nil, outputKey: KeyPath, + isTruncatedKey: KeyPath? = nil, paginationFunction: @escaping (OperationStackInput) async throws -> OperationStackOutput) { self.input = input self.inputKey = inputKey self.outputKey = outputKey + self.isTruncatedKey = isTruncatedKey self.paginationFunction = paginationFunction } @@ -45,6 +48,16 @@ public struct PaginatorSequence \$N<\$N, \$N> {", "}", + "public func \$LPaginated(input: \$N) -> \$N<\$N, \$N> {", + "}", operationShape.toLowerCamelCase(), inputSymbol, ClientRuntimeTypes.Core.PaginatorSequence, inputSymbol, - outputSymbol + outputSymbol, ) { + val isTruncatedFlag = outputShape + .members() + .firstOrNull { it.hasTrait(PaginationTruncationMember.ID) } + ?.defaultName() + + val isTruncatedPart = if (isTruncatedFlag != null) ", isTruncatedKey: \\.$isTruncatedFlag" else "" writer.write( - "return \$N<\$N, \$N>(input: input, inputKey: \\\$N.$markerLiteral, outputKey: \\\$N.$nextMarkerLiteral, paginationFunction: self.\$L(input:))", + "return \$N<\$N, \$N>(input: input, inputKey: \\.$markerLiteral, outputKey: \\.$nextMarkerLiteral$isTruncatedPart, paginationFunction: self.\$L(input:))", ClientRuntimeTypes.Core.PaginatorSequence, inputSymbol, outputSymbol, - inputSymbol, - outputSymbol, - operationShape.toLowerCamelCase() + operationShape.toLowerCamelCase(), ) } } @@ -212,7 +221,7 @@ private data class ItemDescriptor( val collectionLiteral: String, val itemLiteral: String, val itemPathLiteral: String, - val itemSymbol: Symbol + val itemSymbol: Symbol, ) /** @@ -238,6 +247,6 @@ private fun getItemDescriptorOrNull(paginationInfo: PaginationInfo, ctx: Codegen collectionLiteral, itemLiteral, itemPathLiteral, - ctx.symbolProvider.toSymbol(itemMember) + ctx.symbolProvider.toSymbol(itemMember), ) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/customtraits/PaginationTruncationMember.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/customtraits/PaginationTruncationMember.kt new file mode 100644 index 000000000..49ca93f91 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/customtraits/PaginationTruncationMember.kt @@ -0,0 +1,23 @@ + +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.swift.codegen.customtraits + +import software.amazon.smithy.model.node.Node +import software.amazon.smithy.model.node.ObjectNode +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.traits.AnnotationTrait + +/** + * Indicates the annotated member is a truncation indicator which conveys a non-standard termination condition for + * pagination. + */ +class PaginationTruncationMember(node: ObjectNode) : AnnotationTrait(ID, node) { + companion object { + val ID: ShapeId = ShapeId.from("software.amazon.smithy.swift.codegen.synthetic#paginationTruncationMember") + } + + constructor() : this(Node.objectNode()) +} diff --git a/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt b/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt index b2c611894..762bb3673 100644 --- a/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt +++ b/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt @@ -25,7 +25,7 @@ class PaginatorGeneratorTest { /// - input: A `[ListFunctionsInput]` to start pagination /// - Returns: An `AsyncSequence` that can iterate over `ListFunctionsOutput` public func listFunctionsPaginated(input: ListFunctionsInput) -> ClientRuntime.PaginatorSequence { - return ClientRuntime.PaginatorSequence(input: input, inputKey: \ListFunctionsInput.marker, outputKey: \ListFunctionsOutput.nextMarker, paginationFunction: self.listFunctions(input:)) + return ClientRuntime.PaginatorSequence(input: input, inputKey: \.marker, outputKey: \.nextMarker, paginationFunction: self.listFunctions(input:)) } } @@ -58,7 +58,7 @@ class PaginatorGeneratorTest { /// - input: A `[ListFunctionsInput]` to start pagination /// - Returns: An `AsyncSequence` that can iterate over `ListFunctionsOutput` public func listFunctionsPaginated(input: ListFunctionsInput) -> ClientRuntime.PaginatorSequence { - return ClientRuntime.PaginatorSequence(input: input, inputKey: \ListFunctionsInput.marker, outputKey: \ListFunctionsOutput.nextMarker, paginationFunction: self.listFunctions(input:)) + return ClientRuntime.PaginatorSequence(input: input, inputKey: \.marker, outputKey: \.nextMarker, paginationFunction: self.listFunctions(input:)) } } @@ -100,7 +100,7 @@ class PaginatorGeneratorTest { /// - input: A `[PaginatedMapInput]` to start pagination /// - Returns: An `AsyncSequence` that can iterate over `PaginatedMapOutput` public func paginatedMapPaginated(input: PaginatedMapInput) -> ClientRuntime.PaginatorSequence { - return ClientRuntime.PaginatorSequence(input: input, inputKey: \PaginatedMapInput.nextToken, outputKey: \PaginatedMapOutput.inner?.token, paginationFunction: self.paginatedMap(input:)) + return ClientRuntime.PaginatorSequence(input: input, inputKey: \.nextToken, outputKey: \.inner?.token, paginationFunction: self.paginatedMap(input:)) } } @@ -125,6 +125,18 @@ class PaginatorGeneratorTest { contents.shouldContainOnlyOnce(expectedCode) } + @Test + fun testRenderPaginatorTruncatable() { + val context = setupTests("pagination-truncation.smithy", "software.amazon.smithy.swift.codegen.synthetic#Lambda") + val contents = getFileContents(context.manifest, "/Test/Paginators.swift") + val expected = """ + public func listFunctionsTruncatedPaginated(input: ListFunctionsTruncatedInput) -> ClientRuntime.PaginatorSequence { + return ClientRuntime.PaginatorSequence(input: input, inputKey: \.marker, outputKey: \.nextMarker, isTruncatedKey: \.isTruncated, paginationFunction: self.listFunctionsTruncated(input:)) + } +""" + contents.shouldContainOnlyOnce(expected) + } + private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { val context = TestContext.initContextFrom(smithyFile, serviceShapeId, MockHttpRestJsonProtocolGenerator()) { model -> model.defaultSettings(serviceShapeId, "Test", "2019-12-16", "Test") diff --git a/smithy-swift-codegen/src/test/resources/pagination-truncation.smithy b/smithy-swift-codegen/src/test/resources/pagination-truncation.smithy new file mode 100644 index 000000000..1967f9133 --- /dev/null +++ b/smithy-swift-codegen/src/test/resources/pagination-truncation.smithy @@ -0,0 +1,50 @@ +$version: "1.0" + +namespace software.amazon.smithy.swift.codegen.synthetic + +use aws.protocols#restJson1 + +@trait(selector: "*") +structure paginationTruncationMember { } + +service Lambda { + operations: [ListFunctionsTruncated] +} + +list FunctionConfigurationList { + member: FunctionConfiguration +} + +structure FunctionConfiguration { + functionName: String +} + +@paginated( + inputToken: "marker", + outputToken: "nextMarker", + pageSize: "maxItems" +) +@readonly +@http(method: "GET", uri: "/functions/truncated", code: 200) +operation ListFunctionsTruncated { + input: ListFunctionsRequestTruncated, + output: ListFunctionsResponseTruncated +} + +structure ListFunctionsRequestTruncated { + @httpQuery("FunctionVersion") + functionVersion: String, + @httpQuery("Marker") + marker: String, + @httpQuery("MasterRegion") + masterRegion: String, + @httpQuery("MaxItems") + maxItems: Integer, +} + +structure ListFunctionsResponseTruncated { + Functions: FunctionConfigurationList, + @paginationTruncationMember + IsTruncated: Boolean, + nextMarker: String +} \ No newline at end of file From 2550e0350081a4aa11ff94524b45ab42eedd9015 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Mon, 4 Dec 2023 12:21:47 -0800 Subject: [PATCH 25/37] allow isTruncated to be optional bool (#626) --- Sources/ClientRuntime/Pagination/PaginatorSequence.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ClientRuntime/Pagination/PaginatorSequence.swift b/Sources/ClientRuntime/Pagination/PaginatorSequence.swift index 5ca294378..da92aaa93 100644 --- a/Sources/ClientRuntime/Pagination/PaginatorSequence.swift +++ b/Sources/ClientRuntime/Pagination/PaginatorSequence.swift @@ -12,13 +12,13 @@ public struct PaginatorSequence? let outputKey: KeyPath - var isTruncatedKey: KeyPath? + var isTruncatedKey: KeyPath? let paginationFunction: (OperationStackInput) async throws -> OperationStackOutput public init(input: OperationStackInput, inputKey: KeyPath? = nil, outputKey: KeyPath, - isTruncatedKey: KeyPath? = nil, + isTruncatedKey: KeyPath? = nil, paginationFunction: @escaping (OperationStackInput) async throws -> OperationStackOutput) { self.input = input self.inputKey = inputKey @@ -51,7 +51,7 @@ public struct PaginatorSequence Date: Tue, 5 Dec 2023 16:21:23 +0000 Subject: [PATCH 26/37] chore: Updates version to 0.36.0 --- Package.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.version b/Package.version index 4d8ac4d2e..28f024555 100644 --- a/Package.version +++ b/Package.version @@ -1 +1 @@ -0.35.0 \ No newline at end of file +0.36.0 \ No newline at end of file From 39638f3c2fa8c0d970e0e849e30930beb0a3d462 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Wed, 6 Dec 2023 12:44:44 -0600 Subject: [PATCH 27/37] chore: Run tvOS old & new in CI (#628) --- Package.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index a175ca0ed..483b76d6c 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,9 @@ let package = Package( name: "smithy-swift", platforms: [ .macOS(.v10_15), - .iOS(.v13) + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6) ], products: [ .library(name: "ClientRuntime", targets: ["ClientRuntime"]), From 562a8cc32996f59247d9e57e5940cc57b61a9a42 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Thu, 7 Dec 2023 10:48:23 -0600 Subject: [PATCH 28/37] fix: Fix Package.swift warning on Mac (#629) --- Package.swift | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index 483b76d6c..3a1cc1abb 100644 --- a/Package.swift +++ b/Package.swift @@ -2,6 +2,23 @@ import PackageDescription +// Define libxml2 only on Linux, since it causes warnings +// about "pkgconfig not found" on Mac +#if os(Linux) +let libXML2DependencyOrNil: Target.Dependency? = "libxml2" +let libXML2TargetOrNil: Target? = Target.systemLibrary( + name: "libxml2", + pkgConfig: "libxml-2.0", + providers: [ + .apt(["libxml2 libxml2-dev"]), + .yum(["libxml2 libxml2-devel"]) + ] +) +#else +let libXML2DependencyOrNil: Target.Dependency? = nil +let libXML2TargetOrNil: Target? = nil +#endif + let package = Package( name: "smithy-swift", platforms: [ @@ -37,17 +54,10 @@ let package = Package( dependencies: [ "SmithyReadWrite", "SmithyTimestamps", - .target(name: "libxml2", condition: .when(platforms: [.linux])) - ] - ), - .systemLibrary( - name: "libxml2", - pkgConfig: "libxml-2.0", - providers: [ - .apt(["libxml2 libxml2-dev"]), - .yum(["libxml2 libxml2-devel"]) - ] + libXML2DependencyOrNil + ].compactMap { $0 } ), + libXML2TargetOrNil, .target( name: "SmithyTimestamps" ), @@ -71,5 +81,5 @@ let package = Package( name: "SmithyTestUtilTests", dependencies: ["SmithyTestUtil"] ), - ] + ].compactMap { $0 } ) From 460e51c40b7853bc5254bad86cc70003bfc733f9 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Thu, 7 Dec 2023 13:11:47 -0800 Subject: [PATCH 29/37] chore: refactor HttpBody and ByteStream to be a single class ByteStream (#627) --- .../Middleware/PresignerShimHandler.swift | 2 +- ...ody.swift => HTTP2Stream+ByteStream.swift} | 6 +- .../Networking/Http/HttpBody.swift | 123 ----------------- .../Networking/Http/HttpResponse.swift | 6 +- .../Networking/Http/HttpUrlResponse.swift | 2 +- .../RequestBody/BlobBodyMiddleware.swift | 2 +- .../BlobStreamBodyMiddleware.swift | 2 +- .../RequestBody/BodyMiddleware.swift | 2 +- .../RequestBody/EnumBodyMiddleware.swift | 2 +- .../EventStreamBodyMiddleware.swift | 2 +- .../RequestBody/PayloadBodyMiddleware.swift | 4 +- .../RequestBody/StringBodyMiddleware.swift | 2 +- .../Networking/Http/SdkHttpRequest.swift | 8 +- .../Networking/Http/StreamableHttpBody.swift | 14 +- .../Networking/Streaming/ByteStream.swift | 125 +++++++++++------- .../ExpectedSdkHttpRequest.swift | 8 +- .../RequestTestUtil/HttpRequestTestBase.swift | 22 +-- .../MockSerializeStreamMiddleware.swift | 2 +- .../HttpResponseTestBase.swift | 4 +- .../MiddlewareTests/OperationStackTests.swift | 2 +- .../MiddlewareTests/ProviderTests.swift | 6 +- .../MiddlewareStackTests.swift | 4 +- .../CRTClientEngineIntegrationTests.swift | 20 +-- .../ContentLengthMiddlewareTests.swift | 2 +- .../NetworkingTests/HttpBodyTests.swift | 12 +- .../NetworkingTests/HttpRequestTests.swift | 4 +- .../MockHttpClientEngine.swift | 2 +- .../NetworkingTests/NetworkingTestUtils.swift | 4 +- .../HttpRequestTestBaseTests.swift | 10 +- .../HttpResponseTestBaseTests.swift | 2 +- .../HttpProtocolUnitTestRequestGenerator.kt | 8 +- .../HttpResponseTraitWithHttpPayload.kt | 2 +- .../HttpResponseTraitWithoutHttpPayload.kt | 2 +- .../xml/MemberShapeDecodeXMLGenerator.kt | 2 +- ...tpProtocolUnitTestRequestGeneratorTests.kt | 72 +++++----- .../OutputResponseDeserializerTests.kt | 2 +- 36 files changed, 201 insertions(+), 293 deletions(-) rename Sources/ClientRuntime/Networking/Http/CRT/{HTTP2Stream+HttpBody.swift => HTTP2Stream+ByteStream.swift} (89%) delete mode 100644 Sources/ClientRuntime/Networking/Http/HttpBody.swift diff --git a/Sources/ClientRuntime/Middleware/PresignerShimHandler.swift b/Sources/ClientRuntime/Middleware/PresignerShimHandler.swift index c8903c64d..ec0dbdf66 100644 --- a/Sources/ClientRuntime/Middleware/PresignerShimHandler.swift +++ b/Sources/ClientRuntime/Middleware/PresignerShimHandler.swift @@ -30,7 +30,7 @@ struct PresignerShim: Middleware { Self.MInput == H.Input, Self.MOutput == H.Output { handler(input) - let httpResponse = HttpResponse(body: .none, statusCode: .ok) + let httpResponse = HttpResponse(body: .noStream, statusCode: .ok) return .init(httpResponse: httpResponse, output: output) } } diff --git a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+HttpBody.swift b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift similarity index 89% rename from Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+HttpBody.swift rename to Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift index 63ca64ddd..383aa7260 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+HttpBody.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift @@ -14,11 +14,11 @@ extension HTTP2Stream { return 1024 } - /// Writes the HttpBody to the stream asynchronously + /// Writes the ByteStream to the stream asynchronously /// There is no recommended size for the data to write. The data will be written in chunks of `manualWriteBufferSize` bytes. /// - Parameter body: The body to write /// - Throws: Throws an error if the write fails - func write(body: HttpBody) async throws { + func write(body: ByteStream) async throws { switch body { case .data(let data): try await writeData(data: data ?? .init(), endOfStream: true) @@ -27,7 +27,7 @@ extension HTTP2Stream { try await writeData(data: data, endOfStream: false) } try await writeData(data: .init(), endOfStream: true) - case .none: + case .noStream: try await writeData(data: .init(), endOfStream: true) } } diff --git a/Sources/ClientRuntime/Networking/Http/HttpBody.swift b/Sources/ClientRuntime/Networking/Http/HttpBody.swift deleted file mode 100644 index 4351f6cde..000000000 --- a/Sources/ClientRuntime/Networking/Http/HttpBody.swift +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ -import AwsCommonRuntimeKit - -public enum HttpBody { - case data(Data?) - case stream(Stream) - case none -} - -extension HttpBody: Equatable { - public static func == (lhs: HttpBody, rhs: HttpBody) -> Bool { - switch (lhs, rhs) { - case (.data(let lhsData), .data(let rhsData)): - return lhsData == rhsData - case (.stream(let lhsStream), .stream(let rhsStream)): - return lhsStream === rhsStream - default: - return false - } - } -} - -extension HttpBody { - public init(byteStream: ByteStream) { - switch byteStream { - case .data(let data): - self = .data(data) - case .stream(let stream): - self = .stream(stream) - } - } -} - -public extension HttpBody { - - static var empty: HttpBody { - .data(nil) - } - - /// Returns the data for this `HttpBody`. - /// - /// If the `HttpBody` encloses a `Stream`, the enclosed stream is read to - /// the end. If it is seekable, it seeks to the start of the stream and replays all available data. - func readData() async throws -> Data? { - switch self { - case .data(let data): - return data - case .stream(let stream): - if stream.isSeekable { - try stream.seek(toOffset: 0) - } - return try await stream.readToEndAsync() - case .none: - return nil - } - } - - @available(*, deprecated, message: "This method is deprecated and will soon be removed. Call `readData()` instead.") - func toData() throws -> Data? { - switch self { - case .data(let data): - return data - case .stream(let stream): - if stream.isSeekable { - try stream.seek(toOffset: 0) - } - return try stream.readToEnd() - case .none: - return nil - } - } - - /// Returns true if the http body is `.none` or if the underlying data is nil or is empty. - var isEmpty: Bool { - switch self { - case let .data(data): - return data?.isEmpty ?? true - case let .stream(stream): - return stream.isEmpty - case .none: - return true - } - } -} - -extension HttpBody: CustomDebugStringConvertible { - - public var debugDescription: String { - var bodyAsString: String? - switch self { - case .data(let data): - if let data = data { - bodyAsString = String(data: data, encoding: .utf8) - } else { - bodyAsString = "nil" - } - case .stream(let stream): - // reading a non-seekable stream will consume the stream - // which will impact the ability to read the stream later - // so we only read the stream if it is seekable - if stream.isSeekable { - let currentPosition = stream.position - if let data = try? stream.readToEnd() { - bodyAsString = String(data: data, encoding: .utf8) - } - try? stream.seek(toOffset: currentPosition) - } else { - bodyAsString = """ - Position: \(stream.position) - Length: \(stream.length ?? -1) - IsEmpty: \(stream.isEmpty) - IsSeekable: \(stream.isSeekable) - """ - } - default: - bodyAsString = "nil" - } - return bodyAsString ?? "" - } -} diff --git a/Sources/ClientRuntime/Networking/Http/HttpResponse.swift b/Sources/ClientRuntime/Networking/Http/HttpResponse.swift index 676085630..0399ae810 100644 --- a/Sources/ClientRuntime/Networking/Http/HttpResponse.swift +++ b/Sources/ClientRuntime/Networking/Http/HttpResponse.swift @@ -7,16 +7,16 @@ import AwsCommonRuntimeKit public class HttpResponse: HttpUrlResponse { public var headers: Headers - public var body: HttpBody + public var body: ByteStream public var statusCode: HttpStatusCode - public init(headers: Headers = .init(), statusCode: HttpStatusCode = .processing, body: HttpBody = .none) { + public init(headers: Headers = .init(), statusCode: HttpStatusCode = .processing, body: ByteStream = .noStream) { self.headers = headers self.statusCode = statusCode self.body = body } - public init(headers: Headers = .init(), body: HttpBody, statusCode: HttpStatusCode) { + public init(headers: Headers = .init(), body: ByteStream, statusCode: HttpStatusCode) { self.body = body self.statusCode = statusCode self.headers = headers diff --git a/Sources/ClientRuntime/Networking/Http/HttpUrlResponse.swift b/Sources/ClientRuntime/Networking/Http/HttpUrlResponse.swift index fc8cea0d6..c379e7e24 100644 --- a/Sources/ClientRuntime/Networking/Http/HttpUrlResponse.swift +++ b/Sources/ClientRuntime/Networking/Http/HttpUrlResponse.swift @@ -7,6 +7,6 @@ import AwsCommonRuntimeKit protocol HttpUrlResponse { var headers: Headers { get set } - var body: HttpBody { get set} + var body: ByteStream { get set} var statusCode: HttpStatusCode {get set} } diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift index b046349c1..62e0d48d2 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BlobBodyMiddleware.swift @@ -24,7 +24,7 @@ public struct BlobBodyMiddleware: M Self.MInput == H.Input, Self.MOutput == H.Output, Self.Context == H.Context { - let body = HttpBody.data(Data((input.operationInput[keyPath: keyPath] ?? "").utf8)) + let body = ByteStream.data(Data((input.operationInput[keyPath: keyPath] ?? "").utf8)) input.builder.withBody(body) return try await next.handle(context: context, input: input) } diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift index 6a45a3067..b79606146 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift @@ -16,7 +16,7 @@ import struct Foundation.URLRequest // we need to maintain a reference to this same request while we add headers // in the CRT engine so that is why it's a class public class SdkHttpRequest { - public let body: HttpBody + public let body: ByteStream public let endpoint: Endpoint public let method: HttpMethodType private var additionalHeaders: Headers = Headers() @@ -31,7 +31,7 @@ public class SdkHttpRequest { public init(method: HttpMethodType, endpoint: Endpoint, - body: HttpBody = HttpBody.none) { + body: ByteStream = ByteStream.noStream) { self.method = method self.endpoint = endpoint self.body = body @@ -169,7 +169,7 @@ public class SdkHttpRequestBuilder { var methodType: HttpMethodType = .get var host: String = "" var path: String = "/" - var body: HttpBody = .none + var body: ByteStream = .noStream var queryItems: [URLQueryItem]? var port: Int16 = 443 var protocolType: ProtocolType = .https @@ -219,7 +219,7 @@ public class SdkHttpRequestBuilder { } @discardableResult - public func withBody(_ value: HttpBody) -> SdkHttpRequestBuilder { + public func withBody(_ value: ByteStream) -> SdkHttpRequestBuilder { self.body = value return self } diff --git a/Sources/ClientRuntime/Networking/Http/StreamableHttpBody.swift b/Sources/ClientRuntime/Networking/Http/StreamableHttpBody.swift index dd9ec8b08..61020099c 100644 --- a/Sources/ClientRuntime/Networking/Http/StreamableHttpBody.swift +++ b/Sources/ClientRuntime/Networking/Http/StreamableHttpBody.swift @@ -7,15 +7,15 @@ import AwsCommonRuntimeKit -/// A class that implements the `IStreamable` protocol for `HttpBody`. +/// A class that implements the `IStreamable` protocol for `ByteStream`. /// It acts as a bridge between AWS SDK and CRT. class StreamableHttpBody: IStreamable { var position: Data.Index - let body: HttpBody + let body: ByteStream let logger: SwiftLogger - init(body: HttpBody) { + init(body: ByteStream) { self.body = body switch body { @@ -23,7 +23,7 @@ class StreamableHttpBody: IStreamable { position = data?.startIndex ?? .min case .stream(let stream): position = stream.position - case .none: + case .noStream: position = .min } @@ -39,7 +39,7 @@ class StreamableHttpBody: IStreamable { return UInt64(data?.count ?? 0) case .stream(let stream): return UInt64(stream.length ?? 0) - case .none: + case .noStream: return 0 } } @@ -69,7 +69,7 @@ class StreamableHttpBody: IStreamable { } logger.debug("seeking to offset \(offset) in data") try stream.seek(toOffset: Int(offset)) - case .none: + case .noStream: position = .min } } @@ -99,7 +99,7 @@ class StreamableHttpBody: IStreamable { return nil } return data.count - case .none: + case .noStream: return nil } } diff --git a/Sources/ClientRuntime/Networking/Streaming/ByteStream.swift b/Sources/ClientRuntime/Networking/Streaming/ByteStream.swift index b574054ca..ab0d0492c 100644 --- a/Sources/ClientRuntime/Networking/Streaming/ByteStream.swift +++ b/Sources/ClientRuntime/Networking/Streaming/ByteStream.swift @@ -7,45 +7,12 @@ import AwsCommonRuntimeKit import class Foundation.FileHandle -/// A stream of bytes. public enum ByteStream { - /// A stream of bytes represented as a `Data` object. case data(Data?) - - /// A stream of bytes represented as a `Stream` object. - /// - Note: This representation is recommended for large streams of bytes. case stream(Stream) -} - -extension ByteStream { - /// Returns ByteStream from a Data object. - /// - Parameter data: Data object to be converted to ByteStream. - /// - Returns: ByteStream representation of the Data object. - public static func from(data: Data) -> ByteStream { - return .data(data) - } - - /// Returns ByteStream from a FileHandle object. - /// - Parameter fileHandle: FileHandle object to be converted to ByteStream. - /// - Returns: ByteStream representation of the FileHandle object. - public static func from(fileHandle: FileHandle) -> ByteStream { - return .stream(FileStream(fileHandle: fileHandle)) - } + case noStream - /// Returns ByteStream from a String object. - /// - Parameter stringValue: String object to be converted to ByteStream. - /// - Returns: ByteStream representation of the String object. - public static func from(stringValue: String) -> ByteStream { - return .data(stringValue.data(using: .utf8) ?? Data()) - } -} - -extension ByteStream { - - /// Returns the data for this `ByteStream`. - /// - /// If the `ByteStream` encloses a `Stream`, the enclosed stream is read to - /// the end. If it is seekable, it seeks to the start of the stream and replays all available data. + // Read Data public func readData() async throws -> Data? { switch self { case .data(let data): @@ -55,6 +22,23 @@ extension ByteStream { try stream.seek(toOffset: 0) } return try await stream.readToEndAsync() + case .noStream: + return nil + } + } +} + +extension ByteStream: Equatable { + public static func ==(lhs: ByteStream, rhs: ByteStream) -> Bool { + switch (lhs, rhs) { + case (.data(let lhsData), .data(let rhsData)): + return lhsData == rhsData + case (.stream(let lhsStream), .stream(let rhsStream)): + return lhsStream === rhsStream + case (.noStream, .noStream): + return true + default: + return false } } } @@ -62,26 +46,73 @@ extension ByteStream { extension ByteStream: Codable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - let data = try container.decode(Data.self) - self = .data(data) + if container.decodeNil() { + self = .data(nil) + } else { + let data = try container.decode(Data.self) + self = .data(data) + } } public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - try container.encode(self) + switch self { + case .data(let data): + try container.encode(data) + case .stream: + throw EncodingError.invalidValue( + self, + EncodingError.Context( + codingPath: encoder.codingPath, + debugDescription: "Cannot encode a stream." + ) + ) + case .noStream: + try container.encodeNil() + } } } -extension ByteStream: Equatable { +extension ByteStream { - public static func ==(lhs: ByteStream, rhs: ByteStream) -> Bool { - switch (lhs, rhs) { - case (.data(let lhsData), .data(let rhsData)): - return lhsData == rhsData - case (.stream(let lhsStream), .stream(let rhsStream)): - return lhsStream === rhsStream - default: - return false + // Static property for an empty ByteStream + public static var empty: ByteStream { + .data(nil) + } + + // Returns true if the byte stream is empty + public var isEmpty: Bool { + switch self { + case .data(let data): + return data?.isEmpty ?? true + case .stream(let stream): + return stream.isEmpty + case .noStream: + return true + } + } +} + +extension ByteStream: CustomDebugStringConvertible { + + public var debugDescription: String { + switch self { + case .data(let data): + return data?.description ?? "nil (Data)" + case .stream(let stream): + if stream.isSeekable { + let currentPosition = stream.position + defer { try? stream.seek(toOffset: currentPosition) } + if let data = try? stream.readToEnd() { + return data.description + } else { + return "Stream not readable" + } + } else { + return "Stream (non-seekable, Position: \(stream.position), Length: \(stream.length ?? -1))" + } + case .noStream: + return "nil" } } } diff --git a/Sources/SmithyTestUtil/RequestTestUtil/ExpectedSdkHttpRequest.swift b/Sources/SmithyTestUtil/RequestTestUtil/ExpectedSdkHttpRequest.swift index 8ea7a0a6e..0d20e8fae 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/ExpectedSdkHttpRequest.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/ExpectedSdkHttpRequest.swift @@ -8,7 +8,7 @@ import ClientRuntime public struct ExpectedSdkHttpRequest { - public var body: HttpBody + public var body: ByteStream public var headers: Headers? public var forbiddenHeaders: [String]? public var requiredHeaders: [String]? @@ -26,7 +26,7 @@ public struct ExpectedSdkHttpRequest { queryItems: [URLQueryItem]? = nil, forbiddenQueryItems: [URLQueryItem]? = nil, requiredQueryItems: [URLQueryItem]? = nil, - body: HttpBody = HttpBody.none) { + body: ByteStream = ByteStream.noStream) { self.method = method self.endpoint = endpoint self.headers = headers @@ -49,7 +49,7 @@ public class ExpectedSdkHttpRequestBuilder { var methodType: HttpMethodType = .get var host: String = "" var path: String = "/" - var body: HttpBody = .none + var body: ByteStream = .noStream var queryItems = [URLQueryItem]() var forbiddenQueryItems = [URLQueryItem]() var requiredQueryItems = [URLQueryItem]() @@ -103,7 +103,7 @@ public class ExpectedSdkHttpRequestBuilder { } @discardableResult - public func withBody(_ value: HttpBody) -> ExpectedSdkHttpRequestBuilder { + public func withBody(_ value: ByteStream) -> ExpectedSdkHttpRequestBuilder { self.body = value return self } diff --git a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift index c7e250213..b96d2da25 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift @@ -31,7 +31,7 @@ open class HttpRequestTestBase: XCTestCase { queryParams: [String]? = nil, forbiddenQueryParams: [String]? = nil, requiredQueryParams: [String]? = nil, - body: HttpBody?, + body: ByteStream?, host: String, resolvedHost: String?) -> ExpectedSdkHttpRequest { let builder = ExpectedSdkHttpRequestBuilder() @@ -190,12 +190,12 @@ open class HttpRequestTestBase: XCTestCase { Asserts `HttpRequest` objects match /// - Parameter expected: Expected `HttpRequest` /// - Parameter actual: Actual `HttpRequest` to compare against - /// - Parameter assertEqualHttpBody: Close to assert equality of `HttpBody` components + /// - Parameter assertEqualHttpBody: Close to assert equality of `ByteStream` components */ public func assertEqual( _ expected: ExpectedSdkHttpRequest, _ actual: SdkHttpRequest, - _ assertEqualHttpBody: ((HttpBody?, HttpBody?) async throws -> Void)? = nil, + _ assertEqualHttpBody: ((ByteStream?, ByteStream?) async throws -> Void)? = nil, file: StaticString = #filePath, line: UInt = #line ) async throws { @@ -215,14 +215,14 @@ open class HttpRequestTestBase: XCTestCase { assertRequiredQueryItems(expected.requiredQueryItems, actual.queryItems, file: file, line: line) - // assert the contents of HttpBody match, if no body was on the test, no assertions are to be made about the body + // assert the contents of ByteStream match, if no body was on the test, no assertions are to be made about the body // https://smithy.io/2.0/additional-specs/http-protocol-compliance-tests.html#smithy-test-httprequesttests-trait try await assertEqualHttpBody?(expected.body, actual.body) } public func genericAssertEqualHttpBodyData( - expected: HttpBody, - actual: HttpBody, + expected: ByteStream, + actual: ByteStream, isXML: Bool, isJSON: Bool, _ callback: (Data, Data) -> Void, @@ -241,15 +241,15 @@ open class HttpRequestTestBase: XCTestCase { } } - private func extractData(_ httpBody: HttpBody) throws -> Result { + private func extractData(_ httpBody: ByteStream) throws -> Result { switch httpBody { case .data(let actualData): return .success(actualData) case .stream(let byteStream): let data = try byteStream.readToEnd() return .success(data) - case .none: - return .failure(InternalHttpRequestTestBaseError("HttpBody is not Data Type")) + case .noStream: + return .failure(InternalHttpRequestTestBaseError("ByteStream is not Data Type")) } } @@ -257,10 +257,10 @@ open class HttpRequestTestBase: XCTestCase { if expected == nil && actual == nil { return false } else if expected != nil && actual == nil { - XCTFail("actual data in HttpBody is nil but expected is not") + XCTFail("actual data in ByteStream is nil but expected is not") return false } else if expected == nil && actual != nil { - XCTFail("expected data in HttpBody is nil but actual is not") + XCTFail("expected data in ByteStream is nil but actual is not") return false } return true diff --git a/Sources/SmithyTestUtil/RequestTestUtil/MockSerializeStreamMiddleware.swift b/Sources/SmithyTestUtil/RequestTestUtil/MockSerializeStreamMiddleware.swift index 4f61480ee..331e8295d 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/MockSerializeStreamMiddleware.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/MockSerializeStreamMiddleware.swift @@ -14,7 +14,7 @@ public struct MockSerializeStreamMiddleware: Middleware { where H: Handler, HttpContext == H.Context, SerializeStepInput == H.Input, OperationOutput == H.Output { - input.builder.withBody(.init(byteStream: input.operationInput.body)) + input.builder.withBody(input.operationInput.body) return try await next.handle(context: context, input: input) } diff --git a/Sources/SmithyTestUtil/ResponseTestUtil/HttpResponseTestBase.swift b/Sources/SmithyTestUtil/ResponseTestUtil/HttpResponseTestBase.swift index 2a7ec1b97..cd1da08e1 100644 --- a/Sources/SmithyTestUtil/ResponseTestUtil/HttpResponseTestBase.swift +++ b/Sources/SmithyTestUtil/ResponseTestUtil/HttpResponseTestBase.swift @@ -18,7 +18,7 @@ open class HttpResponseTestBase: XCTestCase { public func buildHttpResponse(code: Int, path: String? = nil, headers: [String: String]? = nil, - content: HttpBody?) -> HttpResponse? { + content: ByteStream?) -> HttpResponse? { var internalHeaders: Headers = Headers() if let headers = headers { @@ -26,7 +26,7 @@ open class HttpResponseTestBase: XCTestCase { } return HttpResponse(headers: internalHeaders, - body: content ?? .none, + body: content ?? .noStream, statusCode: HttpStatusCode(rawValue: Int(code)) ?? HttpStatusCode.badRequest) } diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift index 6582b211a..470283b82 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift @@ -55,7 +55,7 @@ class OperationStackTests: HttpRequestTestBase { next: MockHandler { (_, request) in self.checkOrder(&currExpectCount, 6) XCTAssert(request.headers.value(for: "TestHeaderName1") == "TestHeaderValue1") - let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let httpResponse = HttpResponse(body: ByteStream.noStream, statusCode: HttpStatusCode.ok) let output = OperationOutput(httpResponse: httpResponse) return output }) diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift index 4fdc323f6..1c964de81 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift @@ -32,7 +32,7 @@ class ProviderTests: HttpRequestTestBase { next: MockHandler { (context, request) in XCTAssert(context.getPath() == "/3") - let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let httpResponse = HttpResponse(body: ByteStream.noStream, statusCode: HttpStatusCode.ok) let output = OperationOutput(httpResponse: httpResponse) return output }) @@ -55,7 +55,7 @@ class ProviderTests: HttpRequestTestBase { XCTAssert(request.queryItems?.first(where: { queryItem in queryItem.value == "3" }) != nil) - let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let httpResponse = HttpResponse(body: ByteStream.noStream, statusCode: HttpStatusCode.ok) let output = OperationOutput(httpResponse: httpResponse) return output }) @@ -85,7 +85,7 @@ class ProviderTests: HttpRequestTestBase { XCTAssert(request.headers.headers.first(where: { header in header.value == ["3"] }) != nil) - let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let httpResponse = HttpResponse(body: ByteStream.noStream, statusCode: HttpStatusCode.ok) let output = OperationOutput(httpResponse: httpResponse) return output }) diff --git a/Tests/ClientRuntimeTests/MiddlewareTests/MiddlewareStackTests.swift b/Tests/ClientRuntimeTests/MiddlewareTests/MiddlewareStackTests.swift index f29a33eb0..e9bb69e56 100644 --- a/Tests/ClientRuntimeTests/MiddlewareTests/MiddlewareStackTests.swift +++ b/Tests/ClientRuntimeTests/MiddlewareTests/MiddlewareStackTests.swift @@ -23,7 +23,7 @@ class MiddlewareStackTests: XCTestCase { let result = try await stack.handleMiddleware(context: builtContext, input: MockInput(), next: MockHandler(handleCallback: { (_, input) in XCTAssert(input.headers.value(for: "TestHeaderName1") == "TestHeaderValue1") - let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let httpResponse = HttpResponse(body: ByteStream.noStream, statusCode: HttpStatusCode.ok) let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) let output = OperationOutput(httpResponse: httpResponse, output: mockOutput) @@ -61,7 +61,7 @@ class MiddlewareStackTests: XCTestCase { let result = try await stack.handleMiddleware(context: builtContext, input: MockInput(), next: MockHandler(handleCallback: { (_, input) in XCTAssert(input.headers.value(for: "TestHeaderName2") == "TestHeaderValue2") - let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let httpResponse = HttpResponse(body: ByteStream.noStream, statusCode: HttpStatusCode.ok) let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) let output = OperationOutput(httpResponse: httpResponse, output: mockOutput) diff --git a/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift index e94d56262..62351bd00 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift @@ -49,7 +49,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let encodedData = try encoder.encode(body) let request = SdkHttpRequest(method: .post, endpoint: Endpoint(host: "httpbin.org", path: "/post", headers: headers), - body: HttpBody.data(encodedData)) + body: ByteStream.data(encodedData)) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) XCTAssert(response.statusCode == HttpStatusCode.ok) @@ -62,7 +62,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { headers.add(name: "Host", value: "httpbin.org") let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1024", headers: headers), - body: HttpBody.stream(BufferedStream())) + body: ByteStream.stream(BufferedStream())) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) XCTAssert(response.statusCode == HttpStatusCode.ok) @@ -76,10 +76,10 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1024", headers: headers), - body: HttpBody.stream(BufferedStream())) + body: ByteStream.stream(BufferedStream())) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) - if case let HttpBody.stream(unwrappedStream) = response.body { + if case let ByteStream.stream(unwrappedStream) = response.body { let bodyCount = try await unwrappedStream.readToEndAsync()?.count XCTAssertEqual(bodyCount, 1024) } else { @@ -96,10 +96,10 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1", headers: headers), - body: HttpBody.stream(BufferedStream())) + body: ByteStream.stream(BufferedStream())) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) - if case let HttpBody.stream(unwrappedStream) = response.body { + if case let ByteStream.stream(unwrappedStream) = response.body { let bodyCount = try await unwrappedStream.readToEndAsync()?.count XCTAssertEqual(bodyCount, 1) } else { @@ -116,10 +116,10 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/3000", headers: headers), - body: HttpBody.stream(BufferedStream())) + body: ByteStream.stream(BufferedStream())) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) - if case let HttpBody.stream(unwrappedStream) = response.body { + if case let ByteStream.stream(unwrappedStream) = response.body { let bodyCount = try await unwrappedStream.readToEndAsync()?.count XCTAssertEqual(bodyCount, 3000) } else { @@ -139,7 +139,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .post, endpoint: Endpoint(host: "httpbin.org", path: "/post", headers: headers), - body: HttpBody.stream(BufferedStream(data: encodedData))) + body: ByteStream.stream(BufferedStream(data: encodedData))) let response = try await httpClient.execute(request: request) XCTAssertNotNil(response) XCTAssert(response.statusCode == HttpStatusCode.ok) @@ -169,7 +169,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { } let decodedBody = try JSONDecoder().decode(ResponseWrapper.self, from: data) XCTAssertEqual(decodedBody.json, body) - case .data, .none: + case .data, .noStream: XCTFail("Unexpected response body type") } } diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift index e63e42dd0..5dcf3d20f 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -81,7 +81,7 @@ class ContentLengthMiddlewareTests: XCTestCase { for (key, value) in expectedHeaders { XCTAssert(input.headers.value(for: key) == value, file: file, line: line) } - let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let httpResponse = HttpResponse(body: ByteStream.noStream, statusCode: HttpStatusCode.ok) let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) let output = OperationOutput(httpResponse: httpResponse, output: mockOutput) return output diff --git a/Tests/ClientRuntimeTests/NetworkingTests/HttpBodyTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/HttpBodyTests.swift index 9a080c32c..0403e30bd 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/HttpBodyTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/HttpBodyTests.swift @@ -12,35 +12,35 @@ import AwsCommonRuntimeKit class HttpBodyTests: XCTestCase { func testWhenDataIsEmptyThenIsEmptyIsTrue() { let data = Data() - let body = HttpBody.data(data) + let body = ByteStream.data(data) XCTAssertTrue(body.isEmpty) } func testWhenDataIsNilThenIsEmptyIsTrue() { - let body = HttpBody.data(nil) + let body = ByteStream.data(nil) XCTAssertTrue(body.isEmpty) } func testWhenDataIsNotEmptyThenIsEmptyIsFalse() { let data = "foo".data(using: .utf8)! - let body = HttpBody.data(data) + let body = ByteStream.data(data) XCTAssertFalse(body.isEmpty) } func testWhenStreamIsEmptyThenIsEmptyIsTrue() { _ = BufferedStream(data: .init()) - let body = HttpBody.stream(BufferedStream()) + let body = ByteStream.stream(BufferedStream()) XCTAssertTrue(body.isEmpty) } func testWhenStreamIsNotEmptyThenIsEmptyIsFalse() { let stream = BufferedStream(data: .init("foo".data(using: .utf8)!)) - let body = HttpBody.stream(stream) + let body = ByteStream.stream(stream) XCTAssertFalse(body.isEmpty) } func testWhenBodyIsNoneThenIsEmptyIsTrue() { - let body = HttpBody.none + let body = ByteStream.noStream XCTAssertTrue(body.isEmpty) } } diff --git a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift index c1bc09257..798eeb207 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift @@ -24,7 +24,7 @@ class HttpRequestTests: NetworkingTestUtils { let headers = Headers(["header-item-name": "header-item-value"]) let endpoint = Endpoint(host: "host.com", path: "/", headers: headers) - let httpBody = HttpBody.data(expectedMockRequestData) + let httpBody = ByteStream.data(expectedMockRequestData) let mockHttpRequest = SdkHttpRequest(method: .get, endpoint: endpoint, body: httpBody) mockHttpRequest.withHeader(name: "foo", value: "bar") let httpRequest = try mockHttpRequest.toHttpRequest() @@ -61,7 +61,7 @@ class HttpRequestTests: NetworkingTestUtils { let headers = Headers(["Testname-1": "testvalue-1", "Testname-2": "testvalue-2"]) let endpoint = Endpoint(host: "host.com", path: "/", headers: headers) - let httpBody = HttpBody.data(expectedMockRequestData) + let httpBody = ByteStream.data(expectedMockRequestData) let mockHttpRequest = SdkHttpRequest(method: .get, endpoint: endpoint, body: httpBody) let urlRequest = try await URLRequest(sdkRequest: mockHttpRequest) diff --git a/Tests/ClientRuntimeTests/NetworkingTests/MockHttpClientEngine.swift b/Tests/ClientRuntimeTests/NetworkingTests/MockHttpClientEngine.swift index 8f9c0220c..c6b10541e 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/MockHttpClientEngine.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/MockHttpClientEngine.swift @@ -9,7 +9,7 @@ import AwsCommonRuntimeKit class MockHttpClientEngine: HttpClientEngine { func successHttpResponse(request: SdkHttpRequest) -> HttpResponse { - return HttpResponse(headers: request.headers, body: HttpBody.empty, statusCode: HttpStatusCode.ok) + return HttpResponse(headers: request.headers, body: ByteStream.empty, statusCode: HttpStatusCode.ok) } func execute(request: SdkHttpRequest) async throws -> HttpResponse { diff --git a/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift b/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift index 3459ddcbe..68803be8e 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift @@ -30,7 +30,7 @@ class NetworkingTestUtils: XCTestCase { let headers = Headers(["header-item-name": "header-item-value"]) let endpoint = getMockEndpoint(headers: headers) - let httpBody = HttpBody.data(expectedMockRequestData) + let httpBody = ByteStream.data(expectedMockRequestData) mockHttpDataRequest = SdkHttpRequest(method: .get, endpoint: endpoint, body: httpBody) } @@ -41,7 +41,7 @@ class NetworkingTestUtils: XCTestCase { let headers = Headers(["header-item-name": "header-item-value"]) let endpoint = getMockEndpoint(headers: headers) - let httpBody = HttpBody(byteStream: ByteStream.from(data: expectedMockRequestData)) + let httpBody = ByteStream.data(expectedMockRequestData) mockHttpStreamRequest = SdkHttpRequest(method: .get, endpoint: endpoint, body: httpBody) } diff --git a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift index 130c315c6..9d702ff96 100644 --- a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift +++ b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift @@ -102,7 +102,7 @@ class HttpRequestTestBaseTests: HttpRequestTestBase { Self.MOutput == H.Output { let encoder = context.getEncoder() - let body = HttpBody.data(try encoder.encode(input.operationInput)) + let body = ByteStream.data(try encoder.encode(input.operationInput)) input.builder.withBody(body) return try await next.handle(context: context, input: input) @@ -210,8 +210,8 @@ class HttpRequestTestBaseTests: HttpRequestTestBase { } try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) throws -> Void in - XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") - XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") + XCTAssertNotNil(actualHttpBody, "The actual ByteStream is nil") + XCTAssertNotNil(expectedHttpBody, "The expected ByteStream is nil") try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { (expectedData, actualData) in do { let decoder = JSONDecoder() @@ -224,7 +224,7 @@ class HttpRequestTestBaseTests: HttpRequestTestBase { } }) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try! MockOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output @@ -236,7 +236,7 @@ class HttpRequestTestBaseTests: HttpRequestTestBase { .build() _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler { (_, _) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let mockServiceError = try await MockMiddlewareError.makeError(httpResponse: httpResponse, decoder: context.getDecoder()) throw mockServiceError }) diff --git a/Tests/SmithyTestUtilTests/SmithyTestUtilTests/ResponseTestUtilTests/HttpResponseTestBaseTests.swift b/Tests/SmithyTestUtilTests/SmithyTestUtilTests/ResponseTestUtilTests/HttpResponseTestBaseTests.swift index 6a77fa9f7..00b6ffe15 100644 --- a/Tests/SmithyTestUtilTests/SmithyTestUtilTests/ResponseTestUtilTests/HttpResponseTestBaseTests.swift +++ b/Tests/SmithyTestUtilTests/SmithyTestUtilTests/ResponseTestUtilTests/HttpResponseTestBaseTests.swift @@ -14,7 +14,7 @@ class HttpResponseTestBaseTests: HttpResponseTestBase { let statusCode = 200 let headers = ["headerKey1": "headerValue1", "headerKey2": "headerValue2"] let bodyData = "{\"greeting\": \"Hello There\"}".data(using: .utf8)! - let content = HttpBody.data(bodyData) + let content = ByteStream.data(bodyData) guard let httpResponse = buildHttpResponse(code: statusCode, headers: headers, content: content) else { XCTFail("Failed to build Http Response") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt index 8d3166850..aa0228ef1 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolUnitTestRequestGenerator.kt @@ -118,7 +118,7 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B writer.openBlock("_ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in ", "})") { writer.write("XCTFail(\"Deserialize was mocked out, this should fail\")") - writer.write("let httpResponse = HttpResponse(body: .none, statusCode: .badRequest)") + writer.write("let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest)") writer.write("let serviceError = try await $outputErrorName.makeError(httpResponse: httpResponse, decoder: decoder)") writer.write("throw serviceError") } @@ -143,7 +143,7 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B writer.write(" middleware: MockDeserializeMiddleware<$outputSymbol, $outputErrorName>(") writer.openBlock(" id: \"TestDeserializeMiddleware\"){ context, actual in", "})") { renderBodyAssert(test, inputSymbol, inputShape) - writer.write("let response = HttpResponse(body: HttpBody.none, statusCode: .ok)") + writer.write("let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok)") writer.write("let mockOutput = try await $outputSymbol(httpResponse: response, decoder: nil)") writer.write("let output = OperationOutput<$outputSymbol>(httpResponse: response, output: mockOutput)") writer.write("return output") @@ -156,8 +156,8 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B "try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in", "})" ) { - writer.write("XCTAssertNotNil(actualHttpBody, \"The actual HttpBody is nil\")") - writer.write("XCTAssertNotNil(expectedHttpBody, \"The expected HttpBody is nil\")") + writer.write("XCTAssertNotNil(actualHttpBody, \"The actual ByteStream is nil\")") + writer.write("XCTAssertNotNil(expectedHttpBody, \"The expected ByteStream is nil\")") val expectedData = "expectedData" val actualData = "actualData" val isXML = ctx.service.hasTrait() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithHttpPayload.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithHttpPayload.kt index 0806f82aa..a1cf36540 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithHttpPayload.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithHttpPayload.kt @@ -85,7 +85,7 @@ class HttpResponseTraitWithHttpPayload( writer.write("self.\$L = try stream.readToEnd()", memberName) } writer.dedent() - .write("case .none:") + .write("case .noStream:") .indent() .write("self.\$L = nil", memberName).closeBlock("}") } 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 cd76dd545..c1ab5b8f7 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 @@ -98,7 +98,7 @@ class HttpResponseTraitWithoutHttpPayload( .indent() writer.write("self.\$L = .stream(stream)", memberName) writer.dedent() - .write("case .none:") + .write("case .noStream:") .indent() .write("self.\$L = nil", memberName).closeBlock("}") } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeDecodeXMLGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeDecodeXMLGenerator.kt index 9eb322834..a387117d1 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeDecodeXMLGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/xml/MemberShapeDecodeXMLGenerator.kt @@ -283,7 +283,7 @@ abstract class MemberShapeDecodeXMLGenerator( private fun renderEmptyDataForBlobTarget(memberTarget: Shape, memberName: String) { val isStreaming = memberTarget.hasTrait() - val value = if (isStreaming) "${ClientRuntimeTypes.Core.ByteStream}.from(data: \"\".data(using: .utf8)!)" else "\"\".data(using: .utf8)" + val value = if (isStreaming) "${ClientRuntimeTypes.Core.ByteStream}.data(\"\".data(using: .utf8)!)" else "\"\".data(using: .utf8)" renderAssigningDecodedMember(memberName, "$value") } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt index 39e8e20c7..cc8a41d03 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt @@ -100,8 +100,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in - XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") - XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") + XCTAssertNotNil(actualHttpBody, "The actual ByteStream is nil") + XCTAssertNotNil(expectedHttpBody, "The expected ByteStream is nil") try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(SmokeTestInputBody.self, from: expectedData) @@ -115,14 +115,14 @@ class HttpProtocolUnitTestRequestGeneratorTests { } } }) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await SmokeTestOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await SmokeTestOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) @@ -186,20 +186,20 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in - XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") - XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") + XCTAssertNotNil(actualHttpBody, "The actual ByteStream is nil") + XCTAssertNotNil(expectedHttpBody, "The expected ByteStream is nil") try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in XCTAssertEqual(expectedData, actualData) } }) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await ExplicitStringOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await ExplicitStringOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) @@ -252,14 +252,14 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await EmptyInputAndEmptyOutputOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await EmptyInputAndEmptyOutputOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) @@ -322,8 +322,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in - XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") - XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") + XCTAssertNotNil(actualHttpBody, "The actual ByteStream is nil") + XCTAssertNotNil(expectedHttpBody, "The expected ByteStream is nil") try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(SimpleScalarPropertiesInputBody.self, from: expectedData) @@ -342,14 +342,14 @@ class HttpProtocolUnitTestRequestGeneratorTests { } } }) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await SimpleScalarPropertiesOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await SimpleScalarPropertiesOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) @@ -413,20 +413,20 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in - XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") - XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") + XCTAssertNotNil(actualHttpBody, "The actual ByteStream is nil") + XCTAssertNotNil(expectedHttpBody, "The expected ByteStream is nil") try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in XCTAssertEqual(expectedData, actualData) } }) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await StreamingTraitsOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await StreamingTraitsOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) @@ -486,14 +486,14 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await HttpPrefixHeadersOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await HttpPrefixHeadersOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) @@ -559,8 +559,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in - XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") - XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") + XCTAssertNotNil(actualHttpBody, "The actual ByteStream is nil") + XCTAssertNotNil(expectedHttpBody, "The expected ByteStream is nil") try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(JsonUnionsInputBody.self, from: expectedData) @@ -571,14 +571,14 @@ class HttpProtocolUnitTestRequestGeneratorTests { } } }) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await JsonUnionsOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await JsonUnionsOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) @@ -663,8 +663,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in - XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") - XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") + XCTAssertNotNil(actualHttpBody, "The actual ByteStream is nil") + XCTAssertNotNil(expectedHttpBody, "The expected ByteStream is nil") try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(RecursiveShapesInputBody.self, from: expectedData) @@ -675,14 +675,14 @@ class HttpProtocolUnitTestRequestGeneratorTests { } } }) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await RecursiveShapesOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await RecursiveShapesOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) @@ -755,8 +755,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in - XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") - XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") + XCTAssertNotNil(actualHttpBody, "The actual ByteStream is nil") + XCTAssertNotNil(expectedHttpBody, "The expected ByteStream is nil") try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(InlineDocumentInputBody.self, from: expectedData) @@ -768,14 +768,14 @@ class HttpProtocolUnitTestRequestGeneratorTests { } } }) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await InlineDocumentOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await InlineDocumentOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) @@ -845,8 +845,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in try await self.assertEqual(expected, actual, { (expectedHttpBody, actualHttpBody) -> Void in - XCTAssertNotNil(actualHttpBody, "The actual HttpBody is nil") - XCTAssertNotNil(expectedHttpBody, "The expected HttpBody is nil") + XCTAssertNotNil(actualHttpBody, "The actual ByteStream is nil") + XCTAssertNotNil(expectedHttpBody, "The expected ByteStream is nil") try await self.genericAssertEqualHttpBodyData(expected: expectedHttpBody!, actual: actualHttpBody!, isXML: false, isJSON: true) { expectedData, actualData in do { let expectedObj = try decoder.decode(ClientRuntime.Document.self, from: expectedData) @@ -857,14 +857,14 @@ class HttpProtocolUnitTestRequestGeneratorTests { } } }) - let response = HttpResponse(body: HttpBody.none, statusCode: .ok) + let response = HttpResponse(body: ByteStream.noStream, statusCode: .ok) let mockOutput = try await InlineDocumentAsPayloadOutput(httpResponse: response, decoder: nil) let output = OperationOutput(httpResponse: response, output: mockOutput) return output }) _ = try await operationStack.handleMiddleware(context: context, input: input, next: MockHandler(){ (context, request) in XCTFail("Deserialize was mocked out, this should fail") - let httpResponse = HttpResponse(body: .none, statusCode: .badRequest) + let httpResponse = HttpResponse(body: .noStream, statusCode: .badRequest) let serviceError = try await InlineDocumentAsPayloadOutputError.makeError(httpResponse: httpResponse, decoder: decoder) throw serviceError }) diff --git a/smithy-swift-codegen/src/test/kotlin/serde/awsjson11/OutputResponseDeserializerTests.kt b/smithy-swift-codegen/src/test/kotlin/serde/awsjson11/OutputResponseDeserializerTests.kt index 2f30712c0..262f4eda6 100644 --- a/smithy-swift-codegen/src/test/kotlin/serde/awsjson11/OutputResponseDeserializerTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/serde/awsjson11/OutputResponseDeserializerTests.kt @@ -73,7 +73,7 @@ class OutputDeserializerTests { self.streamingData = .data(data) case .stream(let stream): self.streamingData = .stream(stream) - case .none: + case .noStream: self.streamingData = nil } } From d51e9a60b1b9b4260ccc5437a572caee0fc80a0f Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Tue, 12 Dec 2023 13:44:46 -0800 Subject: [PATCH 30/37] chore: remove sync read in unused data extension (#630) --- .../Data+Extensions.swift | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/Sources/ClientRuntime/PrimitiveTypeExtensions/Data+Extensions.swift b/Sources/ClientRuntime/PrimitiveTypeExtensions/Data+Extensions.swift index c1b8b49cd..62e20e062 100644 --- a/Sources/ClientRuntime/PrimitiveTypeExtensions/Data+Extensions.swift +++ b/Sources/ClientRuntime/PrimitiveTypeExtensions/Data+Extensions.swift @@ -4,32 +4,7 @@ */ import struct Foundation.Data -import class Foundation.InputStream public typealias Data = Foundation.Data -extension Data { - init(reading inputStream: InputStream) throws { - self.init() - inputStream.open() - defer { - inputStream.close() - } - - let bufferSize = 1024 - let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) - defer { - buffer.deallocate() - } - while inputStream.hasBytesAvailable { - let read = inputStream.read(buffer, maxLength: bufferSize) - if read < 0 { - throw inputStream.streamError! - } else if read == 0 { - // EOF - break - } - self.append(buffer, count: read) - } - } -} +// Add extensions here as necessary From 3254b1fa77cd605ba92c56665fb6e5e0b03b778d Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Wed, 13 Dec 2023 11:44:37 -0800 Subject: [PATCH 31/37] update smithy to 1.42.0 (#631) --- gradle.properties | 2 +- .../amazon/smithy/swift/codegen/CodegenVisitor.kt | 2 ++ .../integration/HttpProtocolTestGenerator.kt | 14 ++++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/gradle.properties b/gradle.properties index 4951f5d7d..9e83ed633 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ kotlin.code.style=official # config # codegen -smithyVersion=1.39.0 +smithyVersion=1.42.0 smithyGradleVersion=0.6.0 # kotlin diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt index 063475c90..ed1c3b00d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt @@ -20,6 +20,7 @@ import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.shapes.UnionShape import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.model.traits.SensitiveTrait +import software.amazon.smithy.model.transform.ModelTransformer import software.amazon.smithy.swift.codegen.core.GenerationContext import software.amazon.smithy.swift.codegen.integration.CustomDebugStringConvertibleGenerator import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator @@ -86,6 +87,7 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default() { fun preprocessModel(model: Model): Model { var resolvedModel = model + resolvedModel = ModelTransformer.create().flattenAndRemoveMixins(resolvedModel) resolvedModel = AddOperationShapes.execute(resolvedModel, settings.getService(resolvedModel), settings.moduleName) resolvedModel = RecursiveShapeBoxer.transform(resolvedModel) resolvedModel = NestedShapeTransformer.transform(resolvedModel, settings.getService(resolvedModel)) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt index ed19ec50c..813852d00 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolTestGenerator.kt @@ -39,7 +39,8 @@ class HttpProtocolTestGenerator( private val serdeContext: HttpProtocolUnitTestGenerator.SerdeContext, private val imports: List = listOf(), // list of test IDs to ignore/skip - private val testsToIgnore: Set = setOf() + private val testsToIgnore: Set = setOf(), + private val tagsToIgnore: Set = setOf(), ) { private val LOGGER = Logger.getLogger(javaClass.name) @@ -94,7 +95,7 @@ class HttpProtocolTestGenerator( .getOrNull() ?.getTestCasesFor(AppliesTo.CLIENT) .orEmpty() - val requestTestCases = filterProtocolTestCases(tempTestCases) + val requestTestCases = filterProtocolTestCases(filterProtocolTestCasesByTags(tempTestCases)) if (requestTestCases.isNotEmpty()) { val testClassName = "${operation.toUpperCamelCase()}RequestTest" val testFilename = "./${ctx.settings.testModuleName}/$testClassName.swift" @@ -133,7 +134,7 @@ class HttpProtocolTestGenerator( .getOrNull() ?.getTestCasesFor(AppliesTo.CLIENT) .orEmpty() - val responseTestCases = filterProtocolTestCases(tempResponseTests) + val responseTestCases = filterProtocolTestCases(filterProtocolTestCasesByTags(tempResponseTests)) if (responseTestCases.isNotEmpty()) { val testClassName = "${operation.id.name.capitalize()}ResponseTest" val testFilename = "./${ctx.settings.testModuleName}/$testClassName.swift" @@ -173,7 +174,7 @@ class HttpProtocolTestGenerator( .getOrNull() ?.getTestCasesFor(AppliesTo.CLIENT) .orEmpty() - val testCases = filterProtocolTestCases(tempTestCases) + val testCases = filterProtocolTestCases(filterProtocolTestCasesByTags(tempTestCases)) numTestCases += testCases.count() if (testCases.isNotEmpty()) { // multiple error (tests) may be associated with a single operation, @@ -213,6 +214,11 @@ class HttpProtocolTestGenerator( private fun filterProtocolTestCases(testCases: List): List = testCases.filter { it.protocol == ctx.protocol && it.id !in testsToIgnore } + + private fun filterProtocolTestCasesByTags(testCases: List): List = + testCases.filter { testCase -> + testCase.protocol == ctx.protocol && tagsToIgnore.none { tag -> testCase.hasTag(tag) } + } } private fun serverOnly(shape: Shape): Boolean = shape.hasTag("server-only") From 77b100f1d0ca30dcc1e58e5199d45d6d7cc29b04 Mon Sep 17 00:00:00 2001 From: AWS SDK Swift Automation Date: Wed, 20 Dec 2023 18:39:53 +0000 Subject: [PATCH 32/37] chore: Updates version to 0.37.0 --- Package.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.version b/Package.version index 28f024555..e095bebd3 100644 --- a/Package.version +++ b/Package.version @@ -1 +1 @@ -0.36.0 \ No newline at end of file +0.37.0 \ No newline at end of file From d2223784a547719e4ea4b13392626d7ff629da75 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 22 Dec 2023 07:12:40 -0800 Subject: [PATCH 33/37] chore: Update to aws-crt-swift 0.20.0 (#633) --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 3a1cc1abb..01783ca80 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,7 @@ let package = Package( .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]), ], dependencies: [ - .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.17.0"), + .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.20.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", exact: "0.17.0") ], From f11e2e816b8b7e578872bde38b56578caa69fd90 Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 22 Dec 2023 14:51:13 -0800 Subject: [PATCH 34/37] fix: add back from method with fileHandle (#635) --- .../Networking/Streaming/ByteStream.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/ClientRuntime/Networking/Streaming/ByteStream.swift b/Sources/ClientRuntime/Networking/Streaming/ByteStream.swift index ab0d0492c..6dde06584 100644 --- a/Sources/ClientRuntime/Networking/Streaming/ByteStream.swift +++ b/Sources/ClientRuntime/Networking/Streaming/ByteStream.swift @@ -116,3 +116,13 @@ extension ByteStream: CustomDebugStringConvertible { } } } + +extension ByteStream { + + /// Returns ByteStream from a FileHandle object. + /// - Parameter fileHandle: FileHandle object to be converted to ByteStream. + /// - Returns: ByteStream representation of the FileHandle object. + public static func from(fileHandle: FileHandle) -> ByteStream { + return .stream(FileStream(fileHandle: fileHandle)) + } +} From f9046ac56b88aa65531b2d01dc66cb2faf9c01d3 Mon Sep 17 00:00:00 2001 From: Chan Yoo <55515281+sichanyoo@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:17:38 -0500 Subject: [PATCH 35/37] fix!: Add no-op behavior for initialize methods of logging system. (#637) * Add no-op behavior for initialize methods if it isn't the first time being called. * Make LockingSystem threadsafe. * Make initialize methods async. --------- Co-authored-by: Sichan Yoo --- Sources/ClientRuntime/Logging/SDKLoggingSystem.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift b/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift index a408f5a4e..c4c0d92b6 100644 --- a/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift +++ b/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift @@ -7,15 +7,17 @@ import Logging -public class SDKLoggingSystem { +public actor SDKLoggingSystem { + private static var isInitialized = false private static var factories: [String: SDKLogHandlerFactory] = [:] - public class func add(logHandlerFactory: SDKLogHandlerFactory) { + public static func add(logHandlerFactory: SDKLogHandlerFactory) { let label = logHandlerFactory.label factories[label] = logHandlerFactory } - public class func initialize(defaultLogLevel: SDKLogLevel = .info) { + public static func initialize(defaultLogLevel: SDKLogLevel = .info) async { + if isInitialized { return } else { isInitialized = true } LoggingSystem.bootstrap { label in if let factory = factories[label] { return factory.construct(label: label) @@ -26,7 +28,8 @@ public class SDKLoggingSystem { } } - public class func initialize(logLevel: SDKLogLevel) { + public static func initialize(logLevel: SDKLogLevel) async { + if isInitialized { return } else { isInitialized = true } LoggingSystem.bootstrap { label in var handler = StreamLogHandler.standardOutput(label: label) handler.logLevel = logLevel.toLoggerType() From 922d0663ff94bf4895088f13c6d3afcd91c703fa Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Tue, 9 Jan 2024 16:04:28 -0600 Subject: [PATCH 36/37] feat!: URLSession-based HTTP Client (#636) --- .github/workflows/continuous-integration.yml | 25 +- .../DefaultSDKRuntimeConfiguration.swift | 24 +- .../DefaultMessageDecoderStream.swift | 8 +- .../DefaultMessageEncoderStream.swift | 12 +- .../Networking/Http/CRT/CRTClientEngine.swift | 14 +- .../Http/HttpClientConfiguration.swift | 45 ++- .../Networking/Http/HttpClientEngine.swift | 14 +- .../Middlewares/ContentLengthMiddleware.swift | 5 +- .../Networking/Http/SdkHttpClient.swift | 14 +- .../URLSession/FoundationStreamBridge.swift | 212 ++++++++++++ ...onfiguration+HTTPClientConfiguration.swift | 23 ++ .../URLSession/URLSessionHTTPClient.swift | 313 ++++++++++++++++++ .../Networking/Streaming/BufferedStream.swift | 17 +- .../Networking/Streaming/FileStream.swift | 21 +- .../Networking/Streaming/Stream.swift | 4 +- .../NetworkingTests/HttpClientTests.swift | 2 +- .../CRTClientEngineIntegrationTests.swift | 14 +- .../ContentLengthMiddlewareTests.swift | 4 +- .../MockHttpClientEngine.swift | 4 +- .../Streaming/BufferedStreamTests.swift | 12 +- .../FoundationStreamBridgeTests.swift | 97 ++++++ 21 files changed, 803 insertions(+), 81 deletions(-) create mode 100644 Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift create mode 100644 Sources/ClientRuntime/Networking/Http/URLSession/URLSessionConfiguration+HTTPClientConfiguration.swift create mode 100644 Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift create mode 100644 Tests/ClientRuntimeTests/NetworkingTests/URLSession/FoundationStreamBridgeTests.swift diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index e5c851b65..8cc7e9ff0 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -23,23 +23,29 @@ jobs: - macos-13 xcode: - Xcode_14.0.1 - - Xcode_15.0 + - Xcode_15.1 destination: - - 'platform=iOS Simulator,OS=16.0,name=iPhone 13' - - 'platform=iOS Simulator,OS=17.0,name=iPhone 15' + - 'platform=iOS Simulator,OS=16.0,name=iPhone 14' + - 'platform=iOS Simulator,OS=17.2,name=iPhone 15' + - 'platform=tvOS Simulator,OS=16.0,name=Apple TV 4K (at 1080p) (2nd generation)' + - 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)' - 'platform=OS X' exclude: # Don't run old macOS with new Xcode - runner: macos-12 - xcode: Xcode_15.0 + xcode: Xcode_15.1 # Don't run new macOS with old Xcode - runner: macos-13 xcode: Xcode_14.0.1 - # Don't run old iOS simulator with new Xcode - - destination: 'platform=iOS Simulator,OS=16.0,name=iPhone 13' - xcode: Xcode_15.0 - # Don't run new iOS simulator with old Xcode - - destination: 'platform=iOS Simulator,OS=17.0,name=iPhone 15' + # Don't run old iOS/tvOS simulator with new Xcode + - destination: 'platform=iOS Simulator,OS=16.0,name=iPhone 14' + xcode: Xcode_15.1 + - destination: 'platform=tvOS Simulator,OS=16.0,name=Apple TV 4K (at 1080p) (2nd generation)' + xcode: Xcode_15.1 + # Don't run new iOS/tvOS simulator with old Xcode + - destination: 'platform=iOS Simulator,OS=17.2,name=iPhone 15' + xcode: Xcode_14.0.1 + - destination: 'platform=tvOS Simulator,OS=17.2,name=Apple TV 4K (3rd generation) (at 1080p)' xcode: Xcode_14.0.1 steps: - name: Checkout smithy-swift @@ -89,6 +95,7 @@ jobs: - name: Build & Run smithy-swift Kotlin Unit Tests run: ./gradlew build - name: Build & Run smithy-swift Swift Unit Tests + timeout-minutes: 15 run: | set -o pipefail && \ NSUnbufferedIO=YES xcodebuild \ diff --git a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift index 249bac816..ef4497e05 100644 --- a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift +++ b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift @@ -28,7 +28,7 @@ public struct DefaultSDKRuntimeConfiguration HttpClientEngine { - return CRTClientEngine(config: CRTClientEngineConfig(connectTimeoutMs: timeoutMs)) + /// - Parameter httpClientConfiguration: The configuration for the HTTP client. + /// - Returns: The `CRTClientEngine` client on Mac & Linux platforms, returns `URLSessionHttpClient` on non-Mac Apple platforms. + static func makeClient(httpClientConfiguration: HttpClientConfiguration) -> HTTPClient { + #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + return URLSessionHTTPClient(httpClientConfiguration: httpClientConfiguration) + #else + let connectTimeoutMs = httpClientConfiguration.connectTimeout.map { UInt32($0 * 1_000_000) } + let config = CRTClientEngineConfig(connectTimeoutMs: connectTimeoutMs) + return CRTClientEngine(config: config) + #endif } /// The HTTP client configuration to use when none is provided. diff --git a/Sources/ClientRuntime/EventStream/DefaultMessageDecoderStream.swift b/Sources/ClientRuntime/EventStream/DefaultMessageDecoderStream.swift index f33dcb14a..794d5be76 100644 --- a/Sources/ClientRuntime/EventStream/DefaultMessageDecoderStream.swift +++ b/Sources/ClientRuntime/EventStream/DefaultMessageDecoderStream.swift @@ -11,22 +11,22 @@ extension EventStream { public struct DefaultMessageDecoderStream: MessageDecoderStream { public typealias Element = Event - let stream: Stream + let stream: ReadableStream let messageDecoder: MessageDecoder let responseDecoder: ResponseDecoder - public init(stream: Stream, messageDecoder: MessageDecoder, responseDecoder: ResponseDecoder) { + public init(stream: ReadableStream, messageDecoder: MessageDecoder, responseDecoder: ResponseDecoder) { self.stream = stream self.messageDecoder = messageDecoder self.responseDecoder = responseDecoder } public struct AsyncIterator: AsyncIteratorProtocol { - let stream: Stream + let stream: ReadableStream let messageDecoder: MessageDecoder let responseDecoder: ResponseDecoder - init(stream: Stream, messageDecoder: MessageDecoder, responseDecoder: ResponseDecoder) { + init(stream: ReadableStream, messageDecoder: MessageDecoder, responseDecoder: ResponseDecoder) { self.stream = stream self.messageDecoder = messageDecoder self.responseDecoder = responseDecoder diff --git a/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift b/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift index f2a13f8c4..0f74574e8 100644 --- a/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift +++ b/Sources/ClientRuntime/EventStream/DefaultMessageEncoderStream.swift @@ -114,7 +114,11 @@ extension EventStream { } public func readToEndAsync() async throws -> ClientRuntime.Data? { - fatalError("readToEndAsync() is not supported by AsyncStream backed streams") + var data = Data() + while let moreData = try await readAsync(upToCount: Int.max) { + data.append(moreData) + } + return data } /// Reads up to `count` bytes from the stream asynchronously @@ -167,7 +171,11 @@ extension EventStream { } /// Closing the stream is a no-op because the underlying async stream is not owned by this stream - public func close() throws { + public func close() { + // no-op + } + + public func closeWithError(_ error: Error) { // no-op } } diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index b89c954c6..5d1bd82bf 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -10,7 +10,7 @@ import Glibc import Darwin #endif -public class CRTClientEngine: HttpClientEngine { +public class CRTClientEngine: HTTPClient { actor SerialExecutor { /// Stores the common properties of requests that should share a HTTP connection, such that requests @@ -150,7 +150,7 @@ public class CRTClientEngine: HttpClientEngine { self.serialExecutor = SerialExecutor(config: config) } - public func execute(request: SdkHttpRequest) async throws -> HttpResponse { + public func send(request: SdkHttpRequest) async throws -> HttpResponse { let connectionMgr = try await serialExecutor.getOrCreateConnectionPool(endpoint: request.endpoint) let connection = try await connectionMgr.acquireConnection() @@ -294,13 +294,9 @@ public class CRTClientEngine: HttpClientEngine { self.logger.error("Response encountered an error: \(error)") } - do { - // closing the stream is required to signal to the caller that the response is complete - // and no more data will be received in this stream - try stream.close() - } catch { - self.logger.error("Failed to close stream: \(error)") - } + // closing the stream is required to signal to the caller that the response is complete + // and no more data will be received in this stream + stream.close() } requestOptions.http2ManualDataWrites = http2ManualDataWrites diff --git a/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift b/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift index 68ad52f8b..cd6debecb 100644 --- a/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift +++ b/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift @@ -1,18 +1,47 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import struct Foundation.TimeInterval public class HttpClientConfiguration { - public var protocolType: ProtocolType - // initialize with default headers + + /// The timeout for a request, in seconds. + /// + /// If none is provided, the client will use default values based on the platform. + public var connectTimeout: TimeInterval? + + /// HTTP headers to be submitted with every HTTP request. + /// + /// If none is provided, defaults to no extra headers. public var defaultHeaders: Headers + // add any other properties here you want to give the service operations // control over to be mapped to the Http Client - public init(protocolType: ProtocolType = .https, - defaultHeaders: Headers = Headers()) { + /// The URL scheme to be used for HTTP requests. Supported values are `http` and `https`. + /// + /// If none is provided, the default protocol for the operation will be used + public var protocolType: ProtocolType? + + /// Creates a configuration object for a SDK HTTP client. + /// + /// Not all configuration settings may be followed by all clients. + /// - Parameters: + /// - connectTimeout: The maximum time to wait for a response without receiving any data. + /// - defaultHeaders: HTTP headers to be included with every HTTP request. + /// Note that certain headers may cause your API request to fail. Defaults to no headers. + /// - protocolType: The HTTP scheme (`http` or `https`) to be used for API requests. Defaults to the operation's standard configuration. + public init( + connectTimeout: TimeInterval? = nil, + protocolType: ProtocolType = .https, + defaultHeaders: Headers = Headers() + ) { self.protocolType = protocolType self.defaultHeaders = defaultHeaders + self.connectTimeout = connectTimeout } } diff --git a/Sources/ClientRuntime/Networking/Http/HttpClientEngine.swift b/Sources/ClientRuntime/Networking/Http/HttpClientEngine.swift index f933b051e..6e3bcb0d9 100644 --- a/Sources/ClientRuntime/Networking/Http/HttpClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/HttpClientEngine.swift @@ -4,6 +4,16 @@ */ import AwsCommonRuntimeKit -public protocol HttpClientEngine { - func execute(request: SdkHttpRequest) async throws -> HttpResponse +/// The interface for a client that can be used to perform SDK operations over HTTP. +public protocol HTTPClient { + + /// Executes an HTTP request to perform an SDK operation. + /// + /// The request must be fully formed (i.e. endpoint resolved, signed, etc.) before sending. Modifying the request after signature may + /// result in a rejected request. + /// + /// The request body may be in either the form of in-memory data or an asynchronous data stream. + /// - Parameter request: The HTTP request to be performed. + /// - Returns: An HTTP response for the request. Will throw an error if an error is encountered before the HTTP response is received. + func send(request: SdkHttpRequest) async throws -> HttpResponse } diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index 33f5ccbad..8d5fec5ce 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -40,7 +40,7 @@ public struct ContentLengthMiddleware: Middleware { // Transfer-Encoding can be sent on all Event Streams where length cannot be determined // or on blob Data Streams where requiresLength is true and unsignedPayload is false // Only for HTTP/1.1 requests, will be removed in all HTTP/2 requests - input.headers.update(name: "Transfer-Encoding", value: "Chunked") + input.headers.update(name: "Transfer-Encoding", value: "chunked") } else { let operation = context.attributes.get(key: AttributeKey(name: "Operation")) ?? "Error getting operation name" @@ -49,10 +49,9 @@ public struct ContentLengthMiddleware: Middleware { "Missing content-length for SigV4 signing on operation: \(operation)" throw StreamError.notSupported(errorMessage) } - default: + case .noStream: input.headers.update(name: "Content-Length", value: "0") } - return try await next.handle(context: context, input: input) } diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift index b1dfa735b..fc1f8af06 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift @@ -6,9 +6,9 @@ /// this class will implement Handler per new middleware implementation public class SdkHttpClient { - let engine: HttpClientEngine + let engine: HTTPClient - public init(engine: HttpClientEngine, config: HttpClientConfiguration) { + public init(engine: HTTPClient, config: HttpClientConfiguration) { self.engine = engine } @@ -19,20 +19,20 @@ public class SdkHttpClient { return clientHandler.eraseToAnyHandler() } - func execute(request: SdkHttpRequest) async throws -> HttpResponse { - return try await engine.execute(request: request) + func send(request: SdkHttpRequest) async throws -> HttpResponse { + return try await engine.send(request: request) } } -struct ClientHandler: Handler { - let engine: HttpClientEngine +private struct ClientHandler: Handler { + let engine: HTTPClient func handle(context: HttpContext, input: SdkHttpRequest) async throws -> OperationOutput { let httpResponse: HttpResponse if context.shouldForceH2(), let crtEngine = engine as? CRTClientEngine { httpResponse = try await crtEngine.executeHTTP2Request(request: input) } else { - httpResponse = try await engine.execute(request: input) + httpResponse = try await engine.send(request: input) } return OperationOutput(httpResponse: httpResponse) diff --git a/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift b/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift new file mode 100644 index 000000000..f44e55050 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/URLSession/FoundationStreamBridge.swift @@ -0,0 +1,212 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + +import class Foundation.DispatchQueue +import func Foundation.autoreleasepool +import class Foundation.NSObject +import class Foundation.Stream +import class Foundation.InputStream +import class Foundation.OutputStream +import class Foundation.Thread +import class Foundation.RunLoop +import protocol Foundation.StreamDelegate + +/// Reads data from a smithy-swift native `ReadableStream` and streams the data to a Foundation `InputStream`. +/// +/// Used to permit SDK streaming request bodies to be used with `URLSession`-based HTTP requests. +class FoundationStreamBridge: NSObject, StreamDelegate { + + /// The max number of bytes to buffer internally (and transfer) at any given time. + let bufferSize: Int + + /// A buffer to hold data that has been read from the ReadableStream but not yet written to the OutputStream. + private var buffer: Data + + /// The `ReadableStream` that will serve as the input to this bridge. + /// The bridge will read bytes from this stream and dump them to the Foundation stream + /// pair as they become available. + let readableStream: ReadableStream + + /// A Foundation stream that will carry the bytes read from the readableStream as they become available. + let inputStream: InputStream + + /// A Foundation `OutputStream` that will read from the `ReadableStream` + private let outputStream: OutputStream + + /// Actor used to isolate the stream status from multiple concurrent accesses. + actor ReadableStreamStatus { + + /// `true` if the readable stream has been found to be empty, `false` otherwise. Will flip to `true` if the readable stream is read, + /// and `nil` is returned. + var isEmpty = false + + /// Sets stream status to indicate the stream is empty. + func setIsEmpty() async { + isEmpty = true + } + } + + /// Actor used to isolate the stream status from multiple concurrent accesses. + private var readableStreamStatus = ReadableStreamStatus() + + /// A shared serial DispatchQueue to run the `perform`-on-thread operations. + /// Performing thread operations on an async queue allows Swift concurrency tasks to not block. + private static let queue = DispatchQueue(label: "AWSFoundationStreamBridge") + + /// Foundation Streams require a run loop on which to post callbacks for their delegates. + /// All stream operations should be performed on the same thread as the delegate callbacks. + /// A single shared `Thread` is started and is used to host the RunLoop for all Foundation Stream callbacks. + private static let thread: Thread = { + let thread = Thread { autoreleasepool { RunLoop.current.run() } } + thread.name = "AWSFoundationStreamBridge" + thread.start() + return thread + }() + + // MARK: - init & deinit + + /// Creates a stream bridge taking the passed `ReadableStream` as its input + /// + /// Data will be buffered in an internal, in-memory buffer. The Foundation `InputStream` that exposes `readableStream` + /// is exposed by the `inputStream` property after creation. + /// - Parameters: + /// - readableStream: The `ReadableStream` that serves as the input to the bridge. + /// - bufferSize: The number of bytes in the in-memory buffer. The buffer is allocated for this size no matter if in use or not. + /// Defaults to 4096 bytes. + init(readableStream: ReadableStream, bufferSize: Int = 4096) { + var inputStream: InputStream? + var outputStream: OutputStream? + + // Create a "bound stream pair" of Foundation streams. + // Data written into the output stream will automatically flow to the inputStream for reading. + // The bound streams have a buffer between them of size equal to the buffer held by this bridge. + Foundation.Stream.getBoundStreams( + withBufferSize: bufferSize, inputStream: &inputStream, outputStream: &outputStream + ) + guard let inputStream, let outputStream else { + // Fail with fatalError since this is not a failure that would happen in normal operation. + fatalError("Get pair of bound streams failed. Please file a bug with AWS SDK for Swift.") + } + self.bufferSize = bufferSize + self.buffer = Data(capacity: bufferSize) + self.readableStream = readableStream + self.inputStream = inputStream + self.outputStream = outputStream + } + + // MARK: - Opening & closing + + /// Schedule the output stream on the special thread reserved for stream callbacks. + /// Do not wait to complete opening before returning. + func open() async { + await withCheckedContinuation { continuation in + Self.queue.async { + self.perform(#selector(self.openOnThread), on: Self.thread, with: nil, waitUntilDone: false) + } + continuation.resume() + } + } + + /// Configure the output stream to make StreamDelegate callback to this bridge using the special thread / run loop, and open the output stream. + /// The input stream is not included here. It will be configured by `URLSession` when the HTTP request is initiated. + @objc private func openOnThread() { + outputStream.delegate = self + outputStream.schedule(in: RunLoop.current, forMode: .default) + outputStream.open() + } + + /// Unschedule the output stream on the special stream callback thread. + /// Do not wait to complete closing before returning. + func close() async { + await withCheckedContinuation { continuation in + Self.queue.async { + self.perform(#selector(self.closeOnThread), on: Self.thread, with: nil, waitUntilDone: false) + } + continuation.resume() + } + } + + /// Close the output stream and remove it from the thread / run loop. + @objc private func closeOnThread() { + outputStream.close() + outputStream.remove(from: RunLoop.current, forMode: .default) + outputStream.delegate = nil + } + + // MARK: - Writing to bridge + + /// Tries to read from the readable stream if possible, then transfer the data to the output stream. + private func writeToOutput() async throws { + var data = Data() + if await !readableStreamStatus.isEmpty { + if let newData = try await readableStream.readAsync(upToCount: bufferSize) { + data = newData + } else { + await readableStreamStatus.setIsEmpty() + await close() + } + } + try await writeToOutputStream(data: data) + } + + private class WriteToOutputStreamResult: NSObject { + var data = Data() + var error: Error? + } + + /// Write the passed data to the output stream, using the reserved thread. + private func writeToOutputStream(data: Data) async throws { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + Self.queue.async { + let result = WriteToOutputStreamResult() + result.data = data + let selector = #selector(self.writeToOutputStreamOnThread) + self.perform(selector, on: Self.thread, with: result, waitUntilDone: true) + if let error = result.error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } + + /// Append the new data to the buffer, then write to the output stream. Return any error to the caller using the param object. + @objc private func writeToOutputStreamOnThread(_ result: WriteToOutputStreamResult) { + guard !buffer.isEmpty || !result.data.isEmpty else { return } + buffer.append(result.data) + var writeCount = 0 + buffer.withUnsafeBytes { bufferPtr in + let bytePtr = bufferPtr.bindMemory(to: UInt8.self).baseAddress! + writeCount = outputStream.write(bytePtr, maxLength: buffer.count) + } + if writeCount > 0 { + buffer.removeFirst(writeCount) + } + result.error = outputStream.streamError + } + + // MARK: - StreamDelegate protocol + + /// The stream places this callback when appropriate. Call will be delivered on the special thread / run loop for stream callbacks. + /// `.hasSpaceAvailable` prompts this type to query the readable stream for more data. + @objc func stream(_ aStream: Foundation.Stream, handle eventCode: Foundation.Stream.Event) { + switch eventCode { + case .hasSpaceAvailable: + // Since space is available, try and read from the ReadableStream and + // transfer the data to the Foundation stream pair. + // Use a `Task` to perform the operation within Swift concurrency. + Task { try await writeToOutput() } + default: + break + } + } +} + +#endif diff --git a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionConfiguration+HTTPClientConfiguration.swift b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionConfiguration+HTTPClientConfiguration.swift new file mode 100644 index 000000000..a05af485c --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionConfiguration+HTTPClientConfiguration.swift @@ -0,0 +1,23 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + +import class Foundation.URLSessionConfiguration + +extension URLSessionConfiguration { + + public static func from(httpClientConfiguration: HttpClientConfiguration) -> URLSessionConfiguration { + var config = URLSessionConfiguration.default + if let connectTimeout = httpClientConfiguration.connectTimeout { + config.timeoutIntervalForRequest = connectTimeout + } + return config + } +} + +#endif diff --git a/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift new file mode 100644 index 000000000..53b38fee9 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/URLSession/URLSessionHTTPClient.swift @@ -0,0 +1,313 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) || os(visionOS) + +import class Foundation.InputStream +import class Foundation.NSObject +import class Foundation.NSRecursiveLock +import struct Foundation.URLComponents +import struct Foundation.URLQueryItem +import struct Foundation.URLRequest +import class Foundation.URLResponse +import class Foundation.HTTPURLResponse +import class Foundation.URLSession +import class Foundation.URLSessionConfiguration +import class Foundation.URLSessionTask +import class Foundation.URLSessionDataTask +import protocol Foundation.URLSessionDataDelegate +import AwsCommonRuntimeKit + +/// A client that can be used to make requests to AWS services using `Foundation`'s `URLSession` HTTP client. +/// +/// This client is usable on all Swift platforms that support both the `URLSession` library and Objective-C interoperability features +/// (these are generally the Apple platforms.) +/// +/// Use of this client is recommended on all Apple platforms, and is required on Apple Watch ( see +/// [TN3135: Low-level networking on watchOS](https://developer.apple.com/documentation/technotes/tn3135-low-level-networking-on-watchos) +/// for details about allowable modes of networking on the Apple Watch platform.) +/// +/// On Linux platforms, we recommend using the CRT-based HTTP client for its configurability and performance. +public final class URLSessionHTTPClient: HTTPClient { + + /// Holds a connection's associated resources from the time the connection is executed to when it completes. + private final class Connection { + + /// The `FoundationStreamBridge` for the request body, if any. + /// + /// This reference is stored with the connection so that it may be closed (and its resources disposed of) + /// if the connection fails. + let streamBridge: FoundationStreamBridge? + + /// The continuation for the asynchronous call that was made to initiate this request. + /// + /// Once the initial response is received, the continuation is called, and is subsequently set to `nil` so its + /// resources may be deallocated. + var continuation: CheckedContinuation? + + /// Any error received during a delegate callback for this request. + /// + /// The stored error is thrown back to the caller once the URLSessionDelegate receives + /// `urlSession(_:task:didCompleteWithError)` for this connection. + var error: Error? + + /// A response stream that streams the response back to the caller. Data is buffered in-memory until read by the caller. + let responseStream = BufferedStream() + + /// Creates a new connection object + /// - Parameters: + /// - streamBridge: The `FoundationStreamBridge` for the connection. + /// - continuation: The continuation object for the `execute(request:)` call that initiated this connection. + init(streamBridge: FoundationStreamBridge?, continuation: CheckedContinuation) { + self.streamBridge = streamBridge + self.continuation = continuation + } + } + + /// Provides thread-safe associative storage of `Connection`s keyed by their `URLSessionDataTask`. + private final class Storage: @unchecked Sendable { + + /// Ensure all continuations are resumed before deallocation. + /// + /// This should never happen in practice but is being done defensively. + deinit { + connections.values.forEach { + $0.continuation?.resume(throwing: URLSessionHTTPClientError.unresumedConnection) + } + } + + /// Lock used to enforce exclusive access to this `Storage` object. + private let lock = NSRecursiveLock() + + /// A dictionary of `Connection`s, keyed by the `URLSessionTask` associated with them. + private var connections = [URLSessionTask: Connection]() + + /// Adds a connection to the storage, keyed by its `URLSessionTask`. + func set(_ connection: Connection, for key: URLSessionTask) { + lock.lock() + defer { lock.unlock() } + connections[key] = connection + } + + /// Allows modification of a `Connection` while holding exclusive access to it. + /// + /// Do not keep a reference to the connection outside the scope of `block`, or modify the connection after `block` returns. + func modify(_ key: URLSessionTask, block: (Connection) -> Void) { + lock.lock() + defer { lock.unlock() } + guard let connection = connections[key] else { return } + block(connection) + } + + /// Removes the connection keyed by `key` from storage. + func remove(_ key: URLSessionTask) { + lock.lock() + defer { lock.unlock() } + connections.removeValue(forKey: key) + } + } + + /// Handles URLSession delegate callbacks. + private final class SessionDelegate: NSObject, URLSessionDataDelegate { + + /// Holds connection records for all in-progress connections. + let storage = Storage() + + /// Logger for HTTP-related events. + let logger: LogAgent + + init(logger: LogAgent) { + self.logger = logger + } + + /// Called when the initial response to a HTTP request is received. + /// This callback is made as soon as the initial response + headers is complete. + /// Response body data may continue to stream in after this callback is received. + func urlSession( + _ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse + ) async -> URLSession.ResponseDisposition { + logger.debug("urlSession(_:dataTask:didReceive:) called") + storage.modify(dataTask) { connection in + guard let httpResponse = response as? HTTPURLResponse else { + logger.error("Received non-HTTP urlResponse") + let error = URLSessionHTTPClientError.responseNotHTTP + connection.continuation?.resume(throwing: error) + connection.continuation = nil + return + } + let statusCode = HttpStatusCode(rawValue: httpResponse.statusCode) ?? .insufficientStorage + let httpHeaders: [HTTPHeader] = httpResponse.allHeaderFields.compactMap { (name, value) in + guard let name = name as? String else { return nil } + return HTTPHeader(name: name, value: String(describing: value)) + } + let headers = Headers(httpHeaders: httpHeaders) + let body = ByteStream.stream(connection.responseStream) + let response = HttpResponse(headers: headers, body: body, statusCode: statusCode) + connection.continuation?.resume(returning: response) + connection.continuation = nil + } + return .allow + } + + /// Called when response data is received. + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + logger.debug("urlSession(_:dataTask:didReceive:) called with \(data.count) bytes") + storage.modify(dataTask) { connection in + do { + try connection.responseStream.write(contentsOf: data) + } catch { + connection.error = error + dataTask.cancel() + } + } + } + + /// Called when a HTTP request completes, either successfully or not. + /// If an error occurs, it will be returned here. + /// If the error is returned prior to the initial response, the request fails with an error. + /// If the error is returned after the initial response, the error is used to fail the response stream. + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + logger.debug("urlSession(_:task:didCompleteWithError:) called. \(error == nil ? "Success" : "Failure")") + if let error { logger.debug(" Error: \(error.localizedDescription)") } + storage.modify(task) { connection in + if let error = connection.error ?? error { + if let continuation = connection.continuation { + continuation.resume(throwing: error) + connection.continuation = nil + } else { + connection.responseStream.closeWithError(error) + } + } else { + connection.responseStream.close() + } + + // Close the stream bridge so that its resources are deallocated + Task { await connection.streamBridge?.close() } + } + + // Task is complete & no longer needed. Remove it from storage. + storage.remove(task) + } + } + + /// The `HttpClientConfiguration` for this HTTP client. + let config: HttpClientConfiguration + + /// The `URLSession` used to perform HTTP requests. + let session: URLSession + + /// The delegate object used to handle `URLSessionTask` callbacks. + private let delegate: SessionDelegate + + /// The logger for this HTTP client. + private var logger: LogAgent + + // MARK: - init & deinit + + /// Creates a new `URLSessionHTTPClient`. + /// + /// The client is created with its own internal `URLSession`, which is configured with system defaults and with a private delegate for handling + /// URL task lifecycle events. + /// - Parameter urlsessionConfiguration: The configuration to use for the client's `URLSession`. + public init(httpClientConfiguration: HttpClientConfiguration) { + self.config = httpClientConfiguration + self.logger = SwiftLogger(label: "URLSessionHTTPClient") + self.delegate = SessionDelegate(logger: logger) + var urlsessionConfiguration = URLSessionConfiguration.default + urlsessionConfiguration = URLSessionConfiguration.from(httpClientConfiguration: httpClientConfiguration) + self.session = URLSession(configuration: urlsessionConfiguration, delegate: delegate, delegateQueue: nil) + } + + /// On deallocation, finish any in-process tasks before disposing of the `URLSession`. + deinit { + session.finishTasksAndInvalidate() + } + + // MARK: - HttpClientEngine protocol + + /// Executes the passed HTTP request using Foundation's `URLSession` HTTP client. + /// + /// The request is converted to a `URLRequest`, and (if required) the streaming body is bridged to a Foundation `InputStream` and streamed to + /// the remote server. + /// - Parameter request: The request to be submitted to the server. Fields must be filled in sufficiently to form a valid URL. + /// - Returns: The response to the request. This call may return as soon as a complete response is received but before the body finishes streaming; + /// the response body will continue to stream back to the caller. + public func send(request: SdkHttpRequest) async throws -> HttpResponse { + return try await withCheckedThrowingContinuation { continuation in + + // Get the request stream to use for the body, if any. + let requestStream: ReadableStream? + switch request.body { + case .data(let data): + requestStream = BufferedStream(data: data, isClosed: true) + case .stream(let stream): + requestStream = stream + case .noStream: + requestStream = nil + } + + // If needed, create a stream bridge that streams data from a SDK stream to a Foundation InputStream + // that URLSession can stream its request body from. + let streamBridge = requestStream.map { FoundationStreamBridge(readableStream: $0, bufferSize: 4096) } + + // Create the request (with a streaming body when needed.) + let urlRequest = self.makeURLRequest(from: request, httpBodyStream: streamBridge?.inputStream) + + // Create the data task and associated connection object, then place them in storage. + let dataTask = session.dataTask(with: urlRequest) + let connection = Connection(streamBridge: streamBridge, continuation: continuation) + delegate.storage.set(connection, for: dataTask) + + // Start the HTTP connection and start streaming the request body data + dataTask.resume() + Task { await streamBridge?.open() } + } + } + + // MARK: - Private methods + + /// Create a `URLRequest` for the Smithy operation to be performed. + /// - Parameters: + /// - request: The SDK-native, signed `SdkHttpRequest` ready to be transmitted. + /// - httpBodyStream: A Foundation `InputStream` carrying the HTTP body for this request. + /// - Returns: A `URLRequest` ready to be transmitted by `URLSession` for this operation. + private func makeURLRequest(from request: SdkHttpRequest, httpBodyStream: InputStream?) -> URLRequest { + var components = URLComponents() + components.scheme = config.protocolType?.rawValue ?? request.endpoint.protocolType?.rawValue ?? "https" + components.host = request.endpoint.host + components.percentEncodedPath = request.path + if let queryItems = request.queryItems, !queryItems.isEmpty { + components.percentEncodedQueryItems = queryItems.map { + Foundation.URLQueryItem(name: $0.name, value: $0.value) + } + } + guard let url = components.url else { fatalError("Invalid HTTP request. Please file a bug to report this.") } + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = request.method.rawValue + urlRequest.httpBodyStream = httpBodyStream + for header in request.headers.headers + config.defaultHeaders.headers { + for value in header.value { + urlRequest.addValue(value, forHTTPHeaderField: header.name) + } + } + return urlRequest + } +} + +/// Errors that are particular to the URLSession-based Smithy HTTP client. +public enum URLSessionHTTPClientError: Error { + + /// A non-HTTP response was returned by the server. + /// Please file a bug with aws-sdk-swift if you experience this error. + case responseNotHTTP + + /// A connection was not ended + /// Please file a bug with aws-sdk-swift if you experience this error. + case unresumedConnection +} + +#endif diff --git a/Sources/ClientRuntime/Networking/Streaming/BufferedStream.swift b/Sources/ClientRuntime/Networking/Streaming/BufferedStream.swift index c4e5172b2..531b0e7ea 100644 --- a/Sources/ClientRuntime/Networking/Streaming/BufferedStream.swift +++ b/Sources/ClientRuntime/Networking/Streaming/BufferedStream.swift @@ -66,6 +66,8 @@ public class BufferedStream: Stream { /// Access this value only while `lock` is locked, to prevent simultaneous access. private var _dataCount: Int + private var _error: Error? + /// When locked, this `NSRecursiveLock` grants safe, exclusive access to the properties on this type. /// Note: `NSRecursiveLock` is `@Sendable` so it is safe to use with Swift concurrency. private let lock = NSRecursiveLock() @@ -134,6 +136,7 @@ public class BufferedStream: Stream { // if we're closed and there's no data left, return nil // this will signal the end of the stream if _isClosed && chunk.isEmpty == true { + if let error = _error { throw error } return nil } @@ -202,10 +205,22 @@ public class BufferedStream: Stream { } /// Closes the stream. - public func close() throws { + public func close() { + lock.withLockingClosure { + guard !_isClosed else { return } + _isClosed = true + _length = _dataCount + _serviceReadersIfPossible() + } + } + + /// Closes the stream. + public func closeWithError(_ error: Error) { lock.withLockingClosure { + guard !_isClosed else { return } _isClosed = true _length = _dataCount + _error = error _serviceReadersIfPossible() } } diff --git a/Sources/ClientRuntime/Networking/Streaming/FileStream.swift b/Sources/ClientRuntime/Networking/Streaming/FileStream.swift index 50de948d1..0ccb91069 100644 --- a/Sources/ClientRuntime/Networking/Streaming/FileStream.swift +++ b/Sources/ClientRuntime/Networking/Streaming/FileStream.swift @@ -5,11 +5,14 @@ // SPDX-License-Identifier: Apache-2.0 // -import Foundation +import class Foundation.FileHandle +import class Foundation.NSRecursiveLock -/// A `Stream` that wraps a `FileHandle`. +/// A `Stream` that wraps a `FileHandle` for reading the file. +/// /// - Note: This class is thread-safe. -class FileStream: Stream { +final class FileStream: Stream { + /// Returns the length of the stream, if known var length: Int? { guard let len = try? fileHandle.length() else { @@ -118,13 +121,19 @@ class FileStream: Stream { } /// Closes the stream. - func close() throws { - try lock.withLockingClosure { + func close() { + lock.withLockingClosure { if #available(macOS 11, tvOS 13.4, iOS 13.4, watchOS 6.2, *) { - try fileHandle.close() + try? fileHandle.close() } else { fileHandle.closeFile() } } } + + func closeWithError(_ error: Error) { + // The error is only relevant when streaming to a programmatic consumer, not to disk. + // So close the file handle in this case, and the error is dropped. + close() + } } diff --git a/Sources/ClientRuntime/Networking/Streaming/Stream.swift b/Sources/ClientRuntime/Networking/Streaming/Stream.swift index 90db15acb..2b260182d 100644 --- a/Sources/ClientRuntime/Networking/Streaming/Stream.swift +++ b/Sources/ClientRuntime/Networking/Streaming/Stream.swift @@ -53,7 +53,9 @@ public protocol WriteableStream: AnyObject { func write(contentsOf data: Data) throws /// Closes the stream - func close() throws + func close() + + func closeWithError(_ error: Error) } /// Protocol that provides reading and writing data to a stream diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/HttpClientTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/HttpClientTests.swift index c3aae6967..365d074db 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/HttpClientTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/HttpClientTests.swift @@ -22,7 +22,7 @@ class HttpClientTests: NetworkingTestUtils { } func testExecuteRequest() async throws { - let resp = try await httpClient.execute(request: mockHttpDataRequest) + let resp = try await httpClient.send(request: mockHttpDataRequest) XCTAssertNotNil(resp) XCTAssert(resp.statusCode == HttpStatusCode.ok) } diff --git a/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift index 62351bd00..7b379568a 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift @@ -33,7 +33,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { headers.add(name: "Content-type", value: "application/json") headers.add(name: "Host", value: "httpbin.org") let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/get", headers: headers)) - let response = try await httpClient.execute(request: request) + let response = try await httpClient.send(request: request) XCTAssertNotNil(response) XCTAssert(response.statusCode == HttpStatusCode.ok) @@ -50,7 +50,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .post, endpoint: Endpoint(host: "httpbin.org", path: "/post", headers: headers), body: ByteStream.data(encodedData)) - let response = try await httpClient.execute(request: request) + let response = try await httpClient.send(request: request) XCTAssertNotNil(response) XCTAssert(response.statusCode == HttpStatusCode.ok) } @@ -63,7 +63,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1024", headers: headers), body: ByteStream.stream(BufferedStream())) - let response = try await httpClient.execute(request: request) + let response = try await httpClient.send(request: request) XCTAssertNotNil(response) XCTAssert(response.statusCode == HttpStatusCode.ok) } @@ -77,7 +77,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1024", headers: headers), body: ByteStream.stream(BufferedStream())) - let response = try await httpClient.execute(request: request) + let response = try await httpClient.send(request: request) XCTAssertNotNil(response) if case let ByteStream.stream(unwrappedStream) = response.body { let bodyCount = try await unwrappedStream.readToEndAsync()?.count @@ -97,7 +97,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/1", headers: headers), body: ByteStream.stream(BufferedStream())) - let response = try await httpClient.execute(request: request) + let response = try await httpClient.send(request: request) XCTAssertNotNil(response) if case let ByteStream.stream(unwrappedStream) = response.body { let bodyCount = try await unwrappedStream.readToEndAsync()?.count @@ -117,7 +117,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "httpbin.org", path: "/stream-bytes/3000", headers: headers), body: ByteStream.stream(BufferedStream())) - let response = try await httpClient.execute(request: request) + let response = try await httpClient.send(request: request) XCTAssertNotNil(response) if case let ByteStream.stream(unwrappedStream) = response.body { let bodyCount = try await unwrappedStream.readToEndAsync()?.count @@ -140,7 +140,7 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .post, endpoint: Endpoint(host: "httpbin.org", path: "/post", headers: headers), body: ByteStream.stream(BufferedStream(data: encodedData))) - let response = try await httpClient.execute(request: request) + let response = try await httpClient.send(request: request) XCTAssertNotNil(response) XCTAssert(response.statusCode == HttpStatusCode.ok) } diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift index 5dcf3d20f..337fbe838 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -24,14 +24,14 @@ class ContentLengthMiddlewareTests: XCTestCase { func testTransferEncodingChunkedSetWhenStreamLengthIsNil() async throws { addContentLengthMiddlewareWith(requiresLength: false, unsignedPayload: true) forceEmptyStream() - try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "Chunked"]) + try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "chunked"]) } func testTransferEncodingChunkedSetWithNilTraits() async throws { // default constructor addContentLengthMiddlewareWith(requiresLength: nil, unsignedPayload: nil) forceEmptyStream() - try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "Chunked"]) + try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "chunked"]) } func testContentLengthSetWhenStreamLengthAvailableAndRequiresLengthSet() async throws { diff --git a/Tests/ClientRuntimeTests/NetworkingTests/MockHttpClientEngine.swift b/Tests/ClientRuntimeTests/NetworkingTests/MockHttpClientEngine.swift index c6b10541e..ec26e50df 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/MockHttpClientEngine.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/MockHttpClientEngine.swift @@ -7,12 +7,12 @@ import Foundation import AwsCommonRuntimeKit @testable import ClientRuntime -class MockHttpClientEngine: HttpClientEngine { +class MockHttpClientEngine: HTTPClient { func successHttpResponse(request: SdkHttpRequest) -> HttpResponse { return HttpResponse(headers: request.headers, body: ByteStream.empty, statusCode: HttpStatusCode.ok) } - func execute(request: SdkHttpRequest) async throws -> HttpResponse { + func send(request: SdkHttpRequest) async throws -> HttpResponse { return successHttpResponse(request: request) } } diff --git a/Tests/ClientRuntimeTests/NetworkingTests/Streaming/BufferedStreamTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/Streaming/BufferedStreamTests.swift index 09d409604..9cfa0e63d 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/Streaming/BufferedStreamTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/Streaming/BufferedStreamTests.swift @@ -62,7 +62,7 @@ final class BufferedStreamTests: XCTestCase { func test_read_readsRemainingDataThenNilWhenStreamIsClosed() throws { let subject = BufferedStream(data: testData) - try subject.close() + subject.close() let readData1 = try subject.read(upToCount: Int.max) XCTAssertEqual(testData, readData1) let readData2 = try subject.read(upToCount: Int.max) @@ -73,7 +73,7 @@ final class BufferedStreamTests: XCTestCase { func test_readToEnd_readsToEnd() throws { let subject = BufferedStream(data: testData) - try subject.close() + subject.close() let readData = try subject.readToEnd() XCTAssertEqual(readData, testData) @@ -87,7 +87,7 @@ final class BufferedStreamTests: XCTestCase { func test_readToEndAsync_readsToEnd() async throws { let subject = BufferedStream(data: testData) - try subject.close() + subject.close() let readData = try await subject.readToEndAsync() XCTAssertEqual(readData, testData) @@ -135,7 +135,7 @@ final class BufferedStreamTests: XCTestCase { let subject = BufferedStream(data: testData) let readData1 = try await subject.readAsync(upToCount: 4) XCTAssertEqual(testData[0..<4], readData1) - try subject.close() + subject.close() let readData2 = try await subject.readAsync(upToCount: Int.max) XCTAssertEqual(testData[4...], readData2) let readData3 = try await subject.readAsync(upToCount: Int.max) @@ -169,7 +169,7 @@ final class BufferedStreamTests: XCTestCase { func test_length_returnsInitialLengthAfterStreamCloses() throws { let sut = BufferedStream(data: testData) - try sut.close() + sut.close() XCTAssertEqual(sut.length, testData.count) } @@ -178,7 +178,7 @@ final class BufferedStreamTests: XCTestCase { let sut = BufferedStream(data: testData) try sut.write(contentsOf: additionalData) - try sut.close() + sut.close() XCTAssertEqual(sut.length, testData.count + additionalData.count) } diff --git a/Tests/ClientRuntimeTests/NetworkingTests/URLSession/FoundationStreamBridgeTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/URLSession/FoundationStreamBridgeTests.swift new file mode 100644 index 000000000..e8eeeb490 --- /dev/null +++ b/Tests/ClientRuntimeTests/NetworkingTests/URLSession/FoundationStreamBridgeTests.swift @@ -0,0 +1,97 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Run this unit test on macOS only. +// +// `FoundationStreamBridge` is not usable on Linux because it uses ObjC-interop features. +// +// Unit tests of types that spawn threads are unreliable on Apple-platform simulators. +// Mac tests "run on metal", not a simulator, so they will run reliably. +#if os(macOS) + +import Foundation +import XCTest +@testable import ClientRuntime + +class FoundationStreamBridgeTests: XCTestCase { + + func test_open_streamsAllDataToOutputBuffer() async throws { + + // The maximum size of input streaming data in the tests + let maxDataSize = 16_384 + + // The max size of the buffer for data being streamed. + let maxBufferSize = 2 * maxDataSize + + // Create & fill a buffer with random bytes, for use in later test setup + // Random buffer is reused because creating random data is slow + // We are responsible for deallocating it + let randomBuffer = UnsafeMutablePointer.allocate(capacity: maxDataSize) + defer { randomBuffer.deallocate() } + + for i in 0...allocate(capacity: maxDataSize) + defer { tempBuffer.deallocate() } + + // FoundationStreamBridge is susceptible to spurious bugs due to data races & other + // not readily reproducible causes, so run this test repeatedly to help uncover + // problems + + let numberOfRuns = 100 + + for run in 1...numberOfRuns { + // Run a test for every possible data size up to the maximum + let dataSize = min(run, maxDataSize) + + // The buffer may be as small as 1 byte, up to 2x as big as the data size capped by maxBufferSize + let bufferSize = Int.random(in: 1...min(2 * dataSize, maxBufferSize)) + + // Fill a data buffer with dataSize random numbers + let originalData = Data(bytes: randomBuffer, count: dataSize) + + // Create a stream bridge with our original data & open it + let bufferedStream = BufferedStream(data: originalData, isClosed: true) + let subject = FoundationStreamBridge(readableStream: bufferedStream, bufferSize: bufferSize) + await subject.open() + + // This will hold the data that is bridged from the ReadableStream to the Foundation InputStream + var bridgedData = Data() + + // Open the input stream & read it to either end-of-data or a stream error + subject.inputStream.open() + while ![.atEnd, .error].contains(subject.inputStream.streamStatus) { + + // Copy the input stream to the temp buffer. When count is positive, bytes were read + let count = subject.inputStream.read(tempBuffer, maxLength: bufferSize) + if count > 0 { + // Add the read bytes onto the bridged data + bridgedData.append(tempBuffer, count: count) + } else if count < 0 { + XCTAssertNil(subject.inputStream.streamError) + } + } + // Once the subject is exhausted, all data should have been bridged and the subject may be closed + await subject.close() + + // Close the inputStream as well + subject.inputStream.close() + + // Fail in the event of a stream error + XCTAssertNil(subject.inputStream.streamError, "Stream failed with error: \(subject.inputStream.streamError?.localizedDescription ?? "")") + + // Verify data was all bridged + XCTAssertEqual(bridgedData, originalData, "Run \(run) failed (dataSize: \(dataSize), bufferSize: \(bufferSize)") + } + } +} + +#endif From 5e09502e1b0dc4483d21ddf30747d2982654ebcb Mon Sep 17 00:00:00 2001 From: Sichan Yoo Date: Wed, 10 Jan 2024 11:13:05 -0500 Subject: [PATCH 37/37] Delete missed merge conflict marker. --- Package.version | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Package.version b/Package.version index 5fe4c874b..0f1a7dfc7 100644 --- a/Package.version +++ b/Package.version @@ -1,5 +1 @@ -<<<<<<< HEAD -0.35.0 -======= 0.37.0 ->>>>>>> main