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/.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..01783ca80 100644 --- a/Package.swift +++ b/Package.swift @@ -2,18 +2,39 @@ 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: [ .macOS(.v10_15), - .iOS(.v13) + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6) ], 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"), + .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") ], @@ -21,22 +42,44 @@ 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", + libXML2DependencyOrNil + ].compactMap { $0 } + ), + libXML2TargetOrNil, + .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"] - ) - ] + ), + ].compactMap { $0 } ) diff --git a/Package.version b/Package.version index 4d8ac4d2e..0f1a7dfc7 100644 --- a/Package.version +++ b/Package.version @@ -1 +1 @@ -0.35.0 \ No newline at end of file +0.37.0 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 61da564d8..0f74574e8 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 ) } @@ -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/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/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() 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..ec0dbdf66 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, @@ -30,14 +30,7 @@ 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..2dcf50f9f 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/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/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/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/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/HttpClientConfiguration.swift b/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift index 2ba975596..cd6debecb 100644 --- a/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift +++ b/Sources/ClientRuntime/Networking/Http/HttpClientConfiguration.swift @@ -1,19 +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 - // 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 - 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/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/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index ef39978a2..8d5fec5ce 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" @@ -40,7 +40,7 @@ public struct ContentLengthMiddleware // 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 "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/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 new file mode 100644 index 000000000..62e0d48d2 --- /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 = ByteStream.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..1986b342a --- /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 = 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/RequestBody/BodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BodyMiddleware.swift new file mode 100644 index 000000000..19f28d760 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/BodyMiddleware.swift @@ -0,0 +1,48 @@ +// +// 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 BodyMiddleware: Middleware { + public let id: Swift.String = "BodyMiddleware" + + 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, + input: SerializeStepInput, + next: H) async throws -> OperationOutput + where H: Handler, + Self.MInput == H.Input, + Self.MOutput == H.Output, + Self.Context == H.Context { + do { + let data = try documentWritingClosure(input.operationInput, inputWritingClosure) + let body = ByteStream.data(data) + input.builder.withBody(body) + } 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/EnumBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EnumBodyMiddleware.swift new file mode 100644 index 000000000..f2a0de9b6 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/EnumBodyMiddleware.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 EnumBodyMiddleware: 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 = ByteStream.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..d6e49ca18 --- /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(ByteStream.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..e74369106 --- /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 = ByteStream.data(data) + input.builder.withBody(body) + } else if let defaultBody { + input.builder.withBody(ByteStream.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..ca4e1e1bb --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/RequestBody/StringBodyMiddleware.swift @@ -0,0 +1,34 @@ +// +// 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 = ByteStream.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/Networking/Http/Middlewares/SerializableBodyMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/SerializableBodyMiddleware.swift deleted file mode 100644 index 9e06f6e97..000000000 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/SerializableBodyMiddleware.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -public struct SerializableBodyMiddleware: Middleware { - public let id: Swift.String = "\(String(describing: OperationStackInput.self))BodyMiddleware" - - let xmlName: String? - - public init(xmlName: String? = nil) { - self.xmlName = xmlName - } - - 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 { - let encoder = context.getEncoder() - let data: Data - if let xmlName = xmlName, let xmlEncoder = encoder as? XMLEncoder { - data = try xmlEncoder.encode(input.operationInput, withRootKey: xmlName) - } else { - data = try encoder.encode(input.operationInput) - } - let body = HttpBody.data(data) - input.builder.withBody(body) - } 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/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..fc1f8af06 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpClient.swift @@ -6,41 +6,39 @@ /// 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 } - public func getHandler() -> AnyHandler, - HttpContext> { - let clientHandler = ClientHandler(engine: engine) + public func getHandler() + -> AnyHandler, HttpContext> { + + let clientHandler = ClientHandler(engine: engine) 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 - func handle(context: HttpContext, input: SdkHttpRequest) async throws -> OperationOutput { +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) + return OperationOutput(httpResponse: httpResponse) } typealias Input = SdkHttpRequest - - typealias Output = OperationOutput - + typealias Output = OperationOutput typealias Context = HttpContext } diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift index 57eecae79..45d82a197 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 640a442a4..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,11 +23,10 @@ class StreamableHttpBody: IStreamable { position = data?.startIndex ?? .min case .stream(let stream): position = stream.position - case .none: + case .noStream: position = .min } - /// TODO: simplify logger creation logger = SwiftLogger(label: "HttpContent") } @@ -40,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 } } @@ -70,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 } } @@ -100,7 +99,7 @@ class StreamableHttpBody: IStreamable { return nil } return data.count - case .none: + case .noStream: return nil } } 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/ByteStream.swift b/Sources/ClientRuntime/Networking/Streaming/ByteStream.swift index b574054ca..6dde06584 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,83 @@ 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" } } } + +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)) + } +} 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/Sources/ClientRuntime/Pagination/PaginatorSequence.swift b/Sources/ClientRuntime/Pagination/PaginatorSequence.swift index 7e5d4815f..da92aaa93 100644 --- a/Sources/ClientRuntime/Pagination/PaginatorSequence.swift +++ b/Sources/ClientRuntime/Pagination/PaginatorSequence.swift @@ -5,33 +5,36 @@ // 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 init(input: Input, - inputKey: KeyPath? = nil, - outputKey: KeyPath, - paginationFunction: @escaping (Input) async throws -> Output) { +public struct PaginatorSequence: AsyncSequence + where OperationStackInput.Token: Equatable { + + public typealias Element = OperationStackOutput + let input: OperationStackInput + let inputKey: KeyPath? + 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 } 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, @@ -45,6 +48,16 @@ where Input.Token: Equatable { if token != nil && token == input[keyPath: sequence.inputKey!] { break } + + // Use isTruncatedKey from the sequence to check if pagination should continue + if let isTruncatedKey = sequence.isTruncatedKey { + let isTruncated = output[keyPath: isTruncatedKey] ?? false + if !isTruncated { + // set token to nil to break out of the next iteration + token = nil + } + } + return output } return nil 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/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 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/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 9fb67aade..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,15 +215,16 @@ 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, - _ encoder: Any, + expected: ByteStream, + actual: ByteStream, + isXML: Bool, + isJSON: Bool, _ callback: (Data, Data) -> Void, file: StaticString = #filePath, line: UInt = #line @@ -231,24 +232,24 @@ 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!) } } - 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")) } } @@ -256,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/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/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/OperationStackTests.swift index 661537eef..470283b82 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) })) @@ -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 bc910d160..1c964de81 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift @@ -24,15 +24,15 @@ 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, 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 }) @@ -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, @@ -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 }) @@ -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, @@ -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/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/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..e9bb69e56 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, @@ -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) @@ -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,11 +57,11 @@ 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") - 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) @@ -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/CRTClientEngineIntegrationTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/CRTClientEngineIntegrationTests.swift index e94d56262..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) @@ -49,8 +49,8 @@ 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)) - let response = try await httpClient.execute(request: request) + body: ByteStream.data(encodedData)) + let response = try await httpClient.send(request: request) XCTAssertNotNil(response) XCTAssert(response.statusCode == HttpStatusCode.ok) } @@ -62,8 +62,8 @@ 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())) - let response = try await httpClient.execute(request: request) + body: ByteStream.stream(BufferedStream())) + let response = try await httpClient.send(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())) - let response = try await httpClient.execute(request: request) + body: ByteStream.stream(BufferedStream())) + let response = try await httpClient.send(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())) - let response = try await httpClient.execute(request: request) + body: ByteStream.stream(BufferedStream())) + let response = try await httpClient.send(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())) - let response = try await httpClient.execute(request: request) + body: ByteStream.stream(BufferedStream())) + let response = try await httpClient.send(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,8 +139,8 @@ class CRTClientEngineIntegrationTests: NetworkingTestUtils { let request = SdkHttpRequest(method: .post, endpoint: Endpoint(host: "httpbin.org", path: "/post", headers: headers), - body: HttpBody.stream(BufferedStream(data: encodedData))) - let response = try await httpClient.execute(request: request) + body: ByteStream.stream(BufferedStream(data: encodedData))) + let response = try await httpClient.send(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 e2b48fe23..337fbe838 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,20 +18,20 @@ 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 { 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 { @@ -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 @@ -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/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..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: HttpBody.empty, statusCode: HttpStatusCode.ok) + 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/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/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 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 c97847720..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) @@ -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()) @@ -210,9 +210,9 @@ 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 + 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() let expectedObj = try decoder.decode(SayHelloInputBody.self, from: expectedData) @@ -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/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/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/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/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/ClientRuntimeTypes.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt index 7e9ec15ee..31d3b07a7 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,12 +73,19 @@ 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 SigningMiddleware = runtimeSymbol("SignerMiddleware") val AuthSchemeMiddleware = runtimeSymbol("AuthSchemeMiddleware") + 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/CodegenVisitor.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt index 5ee3b05e0..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 @@ -27,6 +28,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 @@ -85,9 +87,11 @@ 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)) + resolvedModel = UnionIndirectivizer.transform(resolvedModel) return resolvedModel } 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..a6cd1b3b3 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 @@ -13,9 +13,11 @@ import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.Shape import software.amazon.smithy.model.traits.PaginatedTrait import software.amazon.smithy.swift.codegen.core.CodegenContext +import software.amazon.smithy.swift.codegen.customtraits.PaginationTruncationMember import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.SwiftIntegration import software.amazon.smithy.swift.codegen.model.SymbolProperty +import software.amazon.smithy.swift.codegen.model.defaultName import software.amazon.smithy.swift.codegen.model.expectShape import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.swift.codegen.model.isBoxed @@ -55,7 +57,7 @@ class PaginatorGenerator : SwiftIntegration { service: ServiceShape, paginatedOperation: OperationShape, paginationInfo: PaginationInfo, - itemDesc: ItemDescriptor? + itemDesc: ItemDescriptor?, ) { val serviceSymbol = ctx.symbolProvider.toSymbol(service) val outputSymbol = ctx.symbolProvider.toSymbol(paginationInfo.output) @@ -72,7 +74,7 @@ class PaginatorGenerator : SwiftIntegration { inputSymbol, outputSymbol, paginationInfo, - cursorSymbol + cursorSymbol, ) // Optionally generate paginator when nested item is specified on the trait. @@ -83,7 +85,7 @@ class PaginatorGenerator : SwiftIntegration { paginatedOperation, itemDesc, inputSymbol, - outputSymbol + outputSymbol, ) } } @@ -98,8 +100,10 @@ class PaginatorGenerator : SwiftIntegration { inputSymbol: Symbol, outputSymbol: Symbol, paginationInfo: PaginationInfo, - cursorSymbol: Symbol + cursorSymbol: Symbol, ) { + val outputShape = paginationInfo.output + writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) val nextMarkerLiteral = paginationInfo.outputTokenMemberPath.joinToString(separator = "?.") { it.toLowerCamelCase() @@ -122,21 +126,26 @@ class PaginatorGenerator : SwiftIntegration { this.write(docBody) } writer.openBlock( - "public func \$LPaginated(input: \$N) -> \$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(), ) } } @@ -182,7 +191,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}` @@ -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/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/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..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,53 +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() - // TODO:: fix indentation when `writeInline` retains indent .writeInline("\n)") - - if (recursiveMemberWithTrait) { - writer.dedent() - .writeInline("\n)") - } } private fun unionDecl(writer: SwiftWriter, shape: UnionShape, block: () -> Unit) { @@ -237,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, @@ -246,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(",") } @@ -336,11 +293,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/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/SymbolVisitor.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SymbolVisitor.kt index e7f80eb9d..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()) @@ -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/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/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/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/HttpBindingProtocolGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt index 9c3b72311..3f10832b5 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 @@ -54,7 +54,6 @@ import software.amazon.smithy.swift.codegen.integration.middlewares.OperationInp 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.SigningMiddleware -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 @@ -146,7 +145,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) } } @@ -166,10 +164,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) @@ -229,7 +228,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) { @@ -250,7 +249,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() } @@ -278,7 +277,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("") @@ -480,6 +479,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..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 @@ -54,7 +54,7 @@ open class HttpProtocolClientGenerator( writer.openBlock("{", "}") { val operationStackName = "operation" 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 d4fae0b25..2ed0fd89c 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 @@ -40,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) @@ -49,13 +49,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 } @@ -91,12 +90,13 @@ 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) .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" @@ -111,6 +111,7 @@ class HttpProtocolTestGenerator( writer.addImport(SwiftDependency.XCTest.target) requestTestBuilder + .ctx(ctx) .writer(writer) .model(ctx.model) .symbolProvider(ctx.symbolProvider) @@ -128,12 +129,13 @@ 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) .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" @@ -146,6 +148,7 @@ class HttpProtocolTestGenerator( writer.addImport(SwiftDependency.XCTest.target) responseTestBuilder + .ctx(ctx) .writer(writer) .model(ctx.model) .symbolProvider(ctx.symbolProvider) @@ -163,7 +166,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)) { @@ -171,7 +175,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, @@ -189,6 +193,7 @@ class HttpProtocolTestGenerator( errorTestBuilder .error(error) + .ctx(ctx) .writer(writer) .model(ctx.model) .symbolProvider(ctx.symbolProvider) @@ -210,6 +215,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") 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..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 @@ -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 @@ -101,19 +106,19 @@ 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(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) 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") } @@ -138,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") @@ -151,11 +156,13 @@ 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" - 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/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/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/middlewares/AuthSchemeMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/AuthSchemeMiddleware.kt index 4ecbb0c49..347cfbf7e 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/AuthSchemeMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/AuthSchemeMiddleware.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 @@ -21,6 +22,7 @@ class AuthSchemeMiddleware( 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/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..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 @@ -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,12 +23,27 @@ class DeserializeMiddleware( override val position = MiddlewarePosition.AFTER 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("$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 3fb6e4025..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 @@ -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 { @@ -34,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 a784d072b..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 @@ -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,13 +28,14 @@ class LoggingMiddleware( override val position = MiddlewarePosition.AFTER 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("$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/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..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 @@ -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,13 +24,13 @@ class OperationInputUrlPathMiddleware( override val position = MiddlewarePosition.AFTER override fun render( + ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String ) { 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/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..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 @@ -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,16 +29,15 @@ 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( - "$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/integration/middlewares/SigningMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/SigningMiddleware.kt index 98024fce9..f2b0c0561 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/SigningMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/SigningMiddleware.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 @@ -21,6 +22,7 @@ class SigningMiddleware( 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/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/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/integration/serde/json/MemberShapeDecodeGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/serde/json/MemberShapeDecodeGenerator.kt index 8695ff912..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" @@ -179,11 +174,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/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..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 @@ -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 {") { @@ -288,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") } @@ -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 fe77de300..c5a254d33 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 @@ -3,6 +3,7 @@ package software.amazon.smithy.swift.codegen.middleware import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait 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 @@ -28,6 +29,7 @@ class MiddlewareExecutionGenerator( private val symbolProvider = ctx.symbolProvider fun render( + serviceShape: ServiceShape, op: OperationShape, flowType: ContextAttributeCodegenFlowType = ContextAttributeCodegenFlowType.NORMAL, onError: (SwiftWriter, String) -> Unit, @@ -40,8 +42,8 @@ class MiddlewareExecutionGenerator( renderContextAttributes(op, flowType) } httpProtocolCustomizable.renderEventStreamAttributes(ctx, writer, op) - writer.write("var $operationStackName = \$N<$inputShapeName, $outputShapeName, $operationErrorName>(id: \"${op.toLowerCamelCase()}\")", OperationStack) - renderMiddlewares(op, operationStackName) + writer.write("var $operationStackName = \$N<$inputShapeName, $outputShapeName>(id: \"${op.toLowerCamelCase()}\")", OperationStack) + renderMiddlewares(ctx, op, operationStackName) } private fun renderContextAttributes(op: OperationShape, flowType: ContextAttributeCodegenFlowType) { @@ -85,12 +87,12 @@ 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) } companion object { 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 cloneOperationShape( diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/SymbolExt.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/SymbolExt.kt index c459a3f1d..70ab54dd0 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/SymbolExt.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/model/SymbolExt.kt @@ -8,7 +8,6 @@ package software.amazon.smithy.swift.codegen.model import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.shapes.MemberShape -import software.amazon.smithy.swift.codegen.SwiftDependency import software.amazon.smithy.swift.codegen.removeSurroundingBackticks /** @@ -39,18 +38,6 @@ fun Symbol.isBoxed(): Boolean { }.orElse(false) } -/** - * Obtains the symbol for a recursive symbol to represent the symbol as Box - */ -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/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/ContentMd5MiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt index 250c69e82..763fc3d27 100644 --- a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt @@ -6,48 +6,39 @@ 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) - .withAuthSchemes(value: config.authSchemes!) - .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) - .withUnsignedPayloadTrait(value: false) - .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.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) - 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) - 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 = """ + 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) + .withAuthSchemes(value: config.authSchemes!) + .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) + .withUnsignedPayloadTrait(value: false) + .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.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) + 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) + 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 + } +""" contents.shouldContainOnlyOnce(expectedContents) } private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { 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 65ccdf133..20c0135ba 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -108,43 +108,37 @@ 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) - .withAuthSchemes(value: config.authSchemes!) - .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) - .withUnsignedPayloadTrait(value: false) - .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.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) - 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) - 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 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) + .withAuthSchemes(value: config.authSchemes!) + .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) + .withUnsignedPayloadTrait(value: false) + .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.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) + 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) + 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 + } +""" contents.shouldContainOnlyOnce(expected) } @@ -153,38 +147,37 @@ 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 = - """ - 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) - .withAuthSchemes(value: config.authSchemes!) - .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) - .withUnsignedPayloadTrait(value: true) - .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) - 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) - 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 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) + .withAuthSchemes(value: config.authSchemes!) + .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) + .withUnsignedPayloadTrait(value: true) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) + 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) + 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 + } +""" contents.shouldContainOnlyOnce(expected) } @@ -193,38 +186,37 @@ 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 = - """ - 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) - .withAuthSchemes(value: config.authSchemes!) - .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) - .withUnsignedPayloadTrait(value: true) - .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) - 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) - 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 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) + .withAuthSchemes(value: config.authSchemes!) + .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) + .withUnsignedPayloadTrait(value: true) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) + 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) + 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 + } +""" contents.shouldContainOnlyOnce(expected) } @@ -233,38 +225,37 @@ 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 = - """ - 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) - .withAuthSchemes(value: config.authSchemes!) - .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) - .withUnsignedPayloadTrait(value: true) - .build() - var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) - operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) - 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) - 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 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) + .withAuthSchemes(value: config.authSchemes!) + .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) + .withUnsignedPayloadTrait(value: true) + .build() + var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) + operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) + 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) + 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 + } +""" 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..cc8a41d03 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()) @@ -95,15 +94,15 @@ 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( 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") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + 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) let actualObj = try decoder.decode(SmokeTestInputBody.self, from: actualData) @@ -116,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 }) @@ -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()) @@ -181,26 +180,26 @@ 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( 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") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + 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 }) @@ -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()) @@ -253,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 }) @@ -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()) @@ -317,15 +316,15 @@ 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( 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") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + 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) let actualObj = try decoder.decode(SimpleScalarPropertiesInputBody.self, from: actualData) @@ -343,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 }) @@ -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()) @@ -409,26 +407,26 @@ 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( 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") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + 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 }) @@ -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()) @@ -488,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 }) @@ -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()) @@ -556,15 +553,15 @@ 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( 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") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + 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) let actualObj = try decoder.decode(JsonUnionsInputBody.self, from: actualData) @@ -574,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 }) @@ -594,8 +591,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 +628,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" ) ) ) @@ -654,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()) @@ -665,15 +657,15 @@ 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( 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") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + 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) let actualObj = try decoder.decode(RecursiveShapesInputBody.self, from: actualData) @@ -683,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 }) @@ -703,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: "") @@ -747,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()) @@ -758,15 +749,15 @@ 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( 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") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + 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) let actualObj = try decoder.decode(InlineDocumentInputBody.self, from: actualData) @@ -777,19 +768,20 @@ 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 }) } - """ + } +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -797,8 +789,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: "") @@ -837,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()) @@ -848,15 +839,15 @@ 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( 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") - try await self.genericAssertEqualHttpBodyData(expectedHttpBody!, actualHttpBody!, encoder) { expectedData, actualData in + 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) let actualObj = try decoder.decode(ClientRuntime.Document.self, from: actualData) @@ -866,19 +857,20 @@ 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 }) } - """ + } +""" 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 256a9f1e2..65fd24acc 100644 --- a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt @@ -6,47 +6,38 @@ 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) - .withAuthSchemes(value: config.authSchemes!) - .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) - .withUnsignedPayloadTrait(value: false) - .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.AuthSchemeMiddleware()) - 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) - 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 = """ + 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) + .withAuthSchemes(value: config.authSchemes!) + .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) + .withUnsignedPayloadTrait(value: false) + .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.AuthSchemeMiddleware()) + 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.finalizeStep.intercept(position: .before, middleware: ClientRuntime.SignerMiddleware()) + 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 + } +""" contents.shouldContainOnlyOnce(expectedContents) } private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { diff --git a/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt b/smithy-swift-codegen/src/test/kotlin/PaginatorGeneratorTest.kt index a8cea099d..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:)) } } @@ -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]` @@ -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:)) } } @@ -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)]` @@ -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/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/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) } 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 161d1bbdd..3768030c1 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 @@ -458,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) } } @@ -466,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/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() 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 } } 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/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 } } 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) } 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