diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 64cecb321..e5c851b65 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -7,43 +7,109 @@ on: workflow_dispatch: env: - BUILDER_VERSION: v0.9.11 - BUILDER_SOURCE: releases - # host owned by CRT team to host aws-crt-builder releases. Contact their on-call with any issues - BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net - PACKAGE_NAME: smithy-swift - LINUX_BASE_IMAGE: ubuntu-16-x64 - RUN: ${{ github.run_id }}-${{ github.run_number }} - AWS_SDK_SWIFT_CI_DIR: /Users/runner/work/smithy-swift/smithy-swift/target/build/deps/aws-sdk-swift - AWS_CRT_SWIFT_CI_DIR: /Users/runner/work/smithy-swift/smithy-swift/target/build/deps/aws-crt-swift - SMITHY_SWIFT_CI_DIR: /Users/runner/work/smithy-swift/smithy-swift + AWS_SWIFT_SDK_USE_LOCAL_DEPS: 1 jobs: downstream: - runs-on: macos-13 + runs-on: ${{ matrix.runner }} env: - DEVELOPER_DIR: /Applications/Xcode_14.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/${{ matrix.xcode }}.app/Contents/Developer + strategy: + fail-fast: false + matrix: + # This matrix runs tests on iOS sim & Mac, on oldest & newest supported Xcodes + runner: + - macos-12 + - macos-13 + xcode: + - Xcode_14.0.1 + - Xcode_15.0 + destination: + - 'platform=iOS Simulator,OS=16.0,name=iPhone 13' + - 'platform=iOS Simulator,OS=17.0,name=iPhone 15' + - 'platform=OS X' + exclude: + # Don't run old macOS with new Xcode + - runner: macos-12 + xcode: Xcode_15.0 + # Don't run new macOS with old Xcode + - runner: macos-13 + xcode: Xcode_14.0.1 + # Don't run old iOS simulator with new Xcode + - destination: 'platform=iOS Simulator,OS=16.0,name=iPhone 13' + xcode: Xcode_15.0 + # Don't run new iOS simulator with old Xcode + - destination: 'platform=iOS Simulator,OS=17.0,name=iPhone 15' + xcode: Xcode_14.0.1 steps: - - name: Checkout sources + - name: Checkout smithy-swift uses: actions/checkout@v3 - - uses: actions/cache@v3 + - name: Select aws-sdk-swift branch + run: | + ORIGINAL_REPO_HEAD_REF="$GITHUB_HEAD_REF" \ + DEPENDENCY_REPO_URL="https://github.com/awslabs/aws-sdk-swift.git" \ + ./scripts/ci_steps/select_dependency_branch.sh + - name: Checkout aws-sdk-swift + uses: actions/checkout@v3 + with: + repository: awslabs/aws-sdk-swift + ref: ${{ env.DEPENDENCY_REPO_SHA }} + path: aws-sdk-swift + - name: Move aws-sdk-swift into place + run: mv aws-sdk-swift .. + - name: Cache Gradle + uses: actions/cache@v3 with: path: | ~/.gradle/caches ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + key: 1-${{ runner.os }}-gradle-${{ hashFiles('settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + 1-${{ runner.os }}-gradle-${{ hashFiles('settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }} + 1-${{ runner.os }}-gradle- + - name: Cache Swift + uses: actions/cache@v3 + with: + path: | + ~/Library/Caches/org.swift.swiftpm + ~/.cache/org.swift.swiftpm + key: 1-${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('Package.swift') }} restore-keys: | - ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} - ${{ runner.os }}-gradle- - - uses: actions/setup-java@v3 + 1-${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('Package.swift') }} + 1-${{ runner.os }}-${{ matrix.xcode }}-spm- + - name: Setup Java + uses: actions/setup-java@v3 with: distribution: corretto java-version: 17 - - name: Build and Test ${{ env.PACKAGE_NAME }} Downstream Consumers + - name: Tools Versions + run: | + cd ../aws-sdk-swift + ./scripts/ci_steps/log_tool_versions.sh + - name: Build & Run smithy-swift Kotlin Unit Tests + run: ./gradlew build + - name: Build & Run smithy-swift Swift Unit Tests + run: | + set -o pipefail && \ + NSUnbufferedIO=YES xcodebuild \ + -scheme smithy-swift-Package \ + -destination '${{ matrix.destination }}' \ + test 2>&1 \ + | xcpretty + - name: Prepare aws-sdk-swift Protocol & Unit Tests run: | - python3 -c "from urllib.request import urlretrieve; urlretrieve('${{ env.BUILDER_HOST }}/${{ env.BUILDER_SOURCE }}/${{ env.BUILDER_VERSION }}/builder.pyz?run=${{ env.RUN }}', 'builder')" - chmod a+x builder - AWS_CRT_SWIFT_CI_DIR="${{ env.AWS_CRT_SWIFT_CI_DIR }}" AWS_SDK_SWIFT_CI_DIR="${{ env.AWS_SDK_SWIFT_CI_DIR }}" SMITHY_SWIFT_CI_DIR="${{ env.SMITHY_SWIFT_CI_DIR }}" AWS_SWIFT_SDK_USE_LOCAL_DEPS=1 ./builder build -p ${{ env.PACKAGE_NAME }} --spec downstream + cd ../aws-sdk-swift + ./scripts/ci_steps/prepare_protocol_and_unit_tests.sh + - name: Build and Run aws-sdk-swift Protocol & Unit Tests + run: | + cd ../aws-sdk-swift + set -o pipefail && \ + NSUnbufferedIO=YES xcodebuild \ + -scheme aws-sdk-swift \ + -destination '${{ matrix.destination }}' \ + test 2>&1 \ + | xcpretty + linux: runs-on: ubuntu-latest strategy: @@ -57,14 +123,41 @@ jobs: container: image: swift:${{ matrix.swift }} steps: + - name: Checkout Sources + uses: actions/checkout@v3 - name: Install openssl run: | if [ -x "$(command -v apt)" ]; then apt-get update && apt-get install -y libssl-dev else - yum install -y openssl-devel + yum install -y openssl-devel which fi - - name: Checkout Sources - uses: actions/checkout@v3 - - name: Build and Test + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: 1-${{ runner.os }}-gradle-${{ hashFiles('settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }} + restore-keys: | + 1-${{ runner.os }}-gradle-${{ hashFiles('settings.gradle.kts', 'gradle/wrapper/gradle-wrapper.properties') }} + 1-${{ runner.os }}-gradle- + - name: Cache Swift + uses: actions/cache@v3 + with: + path: | + ~/Library/Caches/org.swift.swiftpm + ~/.cache/org.swift.swiftpm + key: 1-${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('Package.swift') }} + restore-keys: | + 1-${{ runner.os }}-${{ matrix.xcode }}-spm-${{ hashFiles('Package.swift') }} + 1-${{ runner.os }}-${{ matrix.xcode }}-spm- + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: corretto + java-version: 17 + - name: Build & Run Kotlin Unit Tests + run: ./gradlew build + - name: Build & Run Swift Unit Tests run: swift test diff --git a/Package.swift b/Package.swift index 1f5c6ead8..36e9d0bc3 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.7 import PackageDescription @@ -13,9 +13,9 @@ let package = Package( .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]) ], dependencies: [ - .package(url: "https://github.com/awslabs/aws-crt-swift.git", .exact("0.13.0")), + .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.17.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), - .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", .exact("0.17.0")) + .package(url: "https://github.com/MaxDesiatov/XMLCoder.git", exact: "0.17.0") ], targets: [ .target( diff --git a/Package.version b/Package.version index f0e1a438f..4d8ac4d2e 100644 --- a/Package.version +++ b/Package.version @@ -1 +1 @@ -0.31.0 \ No newline at end of file +0.35.0 \ No newline at end of file diff --git a/README.md b/README.md index b919cec7a..b589f7cbc 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,4 @@ This project is licensed under the Apache-2.0 License. See [CONTRIBUTING](CONTRIBUTING.md) for more information. + diff --git a/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift b/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift new file mode 100644 index 000000000..a8a536121 --- /dev/null +++ b/Sources/ClientRuntime/Idempotency/IdempotencyTokenMiddleware.swift @@ -0,0 +1,33 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +public struct IdempotencyTokenMiddleware: ClientRuntime.Middleware { + public let id: Swift.String = "IdempotencyTokenMiddleware" + private let keyPath: WritableKeyPath + + public init(keyPath: WritableKeyPath) { + self.keyPath = keyPath + } + + public func handle(context: Context, + input: MInput, + next: H) async throws -> MOutput + where H: Handler, Self.MInput == H.Input, Self.MOutput == H.Output, Self.Context == H.Context { + var copiedInput = input + if input[keyPath: keyPath] == nil { + let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator() + copiedInput[keyPath: keyPath] = idempotencyTokenGenerator.generateToken() + } + return try await next.handle(context: context, input: copiedInput) + } + + public typealias MInput = OperationStackInput + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} diff --git a/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift b/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift index 584011547..a408f5a4e 100644 --- a/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift +++ b/Sources/ClientRuntime/Logging/SDKLoggingSystem.swift @@ -15,13 +15,13 @@ public class SDKLoggingSystem { factories[label] = logHandlerFactory } - public class func initialize() { + public class func initialize(defaultLogLevel: SDKLogLevel = .info) { LoggingSystem.bootstrap { label in if let factory = factories[label] { return factory.construct(label: label) } var handler = StreamLogHandler.standardOutput(label: label) - handler.logLevel = .info + handler.logLevel = defaultLogLevel.toLoggerType() return handler } } diff --git a/Sources/ClientRuntime/Networking/Endpoint.swift b/Sources/ClientRuntime/Networking/Endpoint.swift index f51cf8dd5..9dbb65857 100644 --- a/Sources/ClientRuntime/Networking/Endpoint.swift +++ b/Sources/ClientRuntime/Networking/Endpoint.swift @@ -63,10 +63,10 @@ extension Endpoint { public var url: URL? { var components = URLComponents() components.scheme = protocolType?.rawValue - components.host = host + components.host = host.isEmpty ? nil : host // If host is empty, URL is invalid components.percentEncodedPath = path components.percentEncodedQuery = queryItemString - return components.url + return (components.host == nil || components.scheme == nil) ? nil : components.url } var queryItemString: String? { diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 836d35d40..b89c954c6 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -12,12 +12,31 @@ import Darwin public class CRTClientEngine: HttpClientEngine { actor SerialExecutor { + + /// Stores the common properties of requests that should share a HTTP connection, such that requests + /// with equal `ConnectionID` values should be pooled together. + /// + /// Used as a dictionary key for storing CRT connection managers once they have been created. + /// When a new request is made, a connection manager is reused if it matches the request's scheme, + /// host, and port. + private struct ConnectionPoolID: Hashable { + private let protocolType: ProtocolType? + private let host: String + private let port: Int16 + + init(endpoint: Endpoint) { + self.protocolType = endpoint.protocolType + self.host = endpoint.host + self.port = endpoint.port + } + } + private var logger: LogAgent private let windowSize: Int private let maxConnectionsPerEndpoint: Int - private var connectionPools: [Endpoint: HTTPClientConnectionManager] = [:] - private var http2ConnectionPools: [Endpoint: HTTP2StreamManager] = [:] + private var connectionPools: [ConnectionPoolID: HTTPClientConnectionManager] = [:] + private var http2ConnectionPools: [ConnectionPoolID: HTTP2StreamManager] = [:] private let sharedDefaultIO = SDKDefaultIO.shared private let connectTimeoutMs: UInt32? @@ -29,22 +48,22 @@ public class CRTClientEngine: HttpClientEngine { } func getOrCreateConnectionPool(endpoint: Endpoint) throws -> HTTPClientConnectionManager { - guard let connectionPool = connectionPools[endpoint] else { + let poolID = ConnectionPoolID(endpoint: endpoint) + guard let connectionPool = connectionPools[poolID] else { let newConnectionPool = try createConnectionPool(endpoint: endpoint) - connectionPools[endpoint] = newConnectionPool // save in dictionary + connectionPools[poolID] = newConnectionPool // save in dictionary return newConnectionPool } - return connectionPool } func getOrCreateHTTP2ConnectionPool(endpoint: Endpoint) throws -> HTTP2StreamManager { - guard let connectionPool = http2ConnectionPools[endpoint] else { + let poolID = ConnectionPoolID(endpoint: endpoint) + guard let connectionPool = http2ConnectionPools[poolID] else { let newConnectionPool = try createHTTP2ConnectionPool(endpoint: endpoint) - http2ConnectionPools[endpoint] = newConnectionPool // save in dictionary + http2ConnectionPools[poolID] = newConnectionPool // save in dictionary return newConnectionPool } - return connectionPool } diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift index d7e76b9ad..ef39978a2 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentLengthMiddleware.swift @@ -6,7 +6,20 @@ public struct ContentLengthMiddleware private let contentLengthHeaderName = "Content-Length" - public init() {} + private var requiresLength: Bool? + + private var unsignedPayload: Bool? + + /// Creates a new `ContentLengthMiddleware` with the supplied parameters + /// - Parameters: + /// - requiresLength: Trait requires the length of a blob stream to be known. + /// When the request body is not a streaming blob, `nil` should be passed. Defaults to `nil`. + /// - unsignedPayload: Trait signifies that the length of a stream in payload does not need to be known. + /// When the request body is not a streaming blob, `nil` should be passed. Defaults to `nil`. + public init(requiresLength: Bool? = nil, unsignedPayload: Bool? = nil) { + self.requiresLength = requiresLength + self.unsignedPayload = unsignedPayload + } public func handle(context: Context, input: MInput, @@ -22,8 +35,19 @@ public struct ContentLengthMiddleware case .stream(let stream): if let length = stream.length { input.headers.update(name: "Content-Length", value: String(length)) + } else if (requiresLength == false && unsignedPayload == true) || + (requiresLength == nil && unsignedPayload == nil) { + // Transfer-Encoding can be sent on all Event Streams where length cannot be determined + // or on blob Data Streams where requiresLength is true and unsignedPayload is false + // Only for HTTP/1.1 requests, will be removed in all HTTP/2 requests + input.headers.update(name: "Transfer-Encoding", value: "Chunked") } else { - input.headers.update(name: "Transfer-Encoded", value: "Chunked") + let operation = context.attributes.get(key: AttributeKey(name: "Operation")) + ?? "Error getting operation name" + let errorMessage = (unsignedPayload ?? false) ? + "Missing content-length for operation: \(operation)" : + "Missing content-length for SigV4 signing on operation: \(operation)" + throw StreamError.notSupported(errorMessage) } default: input.headers.update(name: "Content-Length", value: "0") diff --git a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift index d6c5febcc..6a45a3067 100644 --- a/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift +++ b/Sources/ClientRuntime/Networking/Http/SdkHttpRequest.swift @@ -6,6 +6,12 @@ import struct Foundation.CharacterSet import struct Foundation.URLQueryItem import struct Foundation.URLComponents import AwsCommonRuntimeKit +// In Linux, Foundation.URLRequest is moved to FoundationNetworking. +#if canImport(FoundationNetworking) +import FoundationNetworking +#else +import struct Foundation.URLRequest +#endif // we need to maintain a reference to this same request while we add headers // in the CRT engine so that is why it's a class @@ -75,6 +81,9 @@ extension SdkHttpRequest { httpRequest.path = [endpoint.path, endpoint.queryItemString].compactMap { $0 }.joined(separator: "?") httpRequest.addHeaders(headers: headers.toHttpHeaders()) + // Remove the "Transfer-Encoding" header if it exists since h2 does not support it + httpRequest.removeHeader(name: "Transfer-Encoding") + // HTTP2Request used with manual writes hence we need to set the body to nil // so that CRT does not write the body for us (we will write it manually) httpRequest.body = nil @@ -82,6 +91,30 @@ extension SdkHttpRequest { } } +public extension URLRequest { + init(sdkRequest: SdkHttpRequest) async throws { + // Set URL + guard let url = sdkRequest.endpoint.url else { + throw ClientError.dataNotFound("Failed to construct URLRequest due to missing URL.") + } + self.init(url: url) + // Set method type + self.httpMethod = sdkRequest.method.rawValue + // Set body, handling any serialization errors + do { + self.httpBody = try await sdkRequest.body.readData() + } catch { + throw ClientError.serializationFailed("Failed to construct URLRequest due to HTTP body conversion failure.") + } + // Set headers + sdkRequest.headers.headers.forEach { header in + header.value.forEach { value in + self.addValue(value, forHTTPHeaderField: header.name) + } + } + } +} + extension SdkHttpRequest: CustomDebugStringConvertible, CustomStringConvertible { public var debugDescriptionWithBody: String { diff --git a/Sources/ClientRuntime/Retries/DefaultRetryStrategy/RetryQuota.swift b/Sources/ClientRuntime/Retries/DefaultRetryStrategy/RetryQuota.swift index 717783dd2..d516d658c 100644 --- a/Sources/ClientRuntime/Retries/DefaultRetryStrategy/RetryQuota.swift +++ b/Sources/ClientRuntime/Retries/DefaultRetryStrategy/RetryQuota.swift @@ -55,7 +55,7 @@ final actor RetryQuota { /// Creates a new quota with settings from the passed options. /// - Parameter options: The retry strategy options from which to configure this retry quota - convenience init(options: RetryStrategyOptions) { + init(options: RetryStrategyOptions) { self.init( availableCapacity: options.availableCapacity, maxCapacity: options.maxCapacity, diff --git a/Sources/ClientRuntime/Util/PlatformOperatingSystem.swift b/Sources/ClientRuntime/Util/PlatformOperatingSystem.swift index 60a1782d3..0caf3b786 100644 --- a/Sources/ClientRuntime/Util/PlatformOperatingSystem.swift +++ b/Sources/ClientRuntime/Util/PlatformOperatingSystem.swift @@ -12,6 +12,7 @@ public enum PlatformOperatingSystem: String { case macOS case watchOS case tvOS + case visionOS case unknown } @@ -28,6 +29,8 @@ public var currentOS: PlatformOperatingSystem { return .windows #elseif os(tvOS) return .tvOS + #elseif os(visionOS) + return .visionOS #else #error("Cannot use a an operating system we do not support") #endif diff --git a/Sources/ClientRuntime/Util/SwiftVersion.swift b/Sources/ClientRuntime/Util/SwiftVersion.swift index 6c167d527..6d1633b2c 100644 --- a/Sources/ClientRuntime/Util/SwiftVersion.swift +++ b/Sources/ClientRuntime/Util/SwiftVersion.swift @@ -49,20 +49,6 @@ private func swift5Version() -> String? { return "5.8" #elseif swift(>=5.7) return "5.7" - #elseif swift(>=5.6) - return "5.6" - #elseif swift(>=5.5) - return "5.5" - #elseif swift(>=5.4) - return "5.4" - #elseif swift(>=5.3) - return "5.3" - #elseif swift(>=5.2) - return "5.2" - #elseif swift(>=5.1) - return "5.1" - #elseif swift(>=5.0) - return "5.0" #else return nil #endif diff --git a/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift b/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift new file mode 100644 index 000000000..1728c396e --- /dev/null +++ b/Tests/ClientRuntimeTests/Idempotency/IdempotencyTokenMiddlewareTests.swift @@ -0,0 +1,86 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import ClientRuntime +import XCTest + +class IdempotencyTokenMiddlewareTests: XCTestCase { + + private typealias Subject = IdempotencyTokenMiddleware + + let token = "def" + let previousToken = "abc" + private var tokenGenerator: IdempotencyTokenGenerator! + private var context: HttpContext! + private var subject: Subject! + + override func setUp() async throws { + try await super.setUp() + tokenGenerator = TestIdempotencyTokenGenerator(token: token) + context = HttpContextBuilder().withIdempotencyTokenGenerator(value: tokenGenerator).build() + subject = Subject(keyPath: \.tokenMember) + } + + func test_handle_itSetsAnIdempotencyTokenIfNoneIsSet() async throws { + let input = TestInputType(tokenMember: nil) + let next = MockHandler { (context, input) in + XCTAssertEqual(input.tokenMember, self.token) + } + _ = try await subject.handle(context: context, input: input, next: next) + } + + func test_handle_itDoesNotChangeTheIdempotencyTokenIfAlreadySet() async throws { + let input = TestInputType(tokenMember: previousToken) + let next = MockHandler { (context, input) in + XCTAssertEqual(input.tokenMember, self.previousToken) + } + _ = try await subject.handle(context: context, input: input, next: next) + } +} + +// MARK: - Test fixtures & types + +private struct TestIdempotencyTokenGenerator: IdempotencyTokenGenerator { + let token: String + func generateToken() -> String { token } +} + +private struct TestInputType { + var tokenMember: String? +} + +private struct TestOutputType: HttpResponseBinding { + init(httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder?) async throws { + // no-op + } +} + +private enum TestOutputErrorType: HttpResponseErrorBinding { + static func makeError(httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder?) async throws -> Error { + return TestError() + } +} + +private struct TestError: Error {} + +private struct MockHandler: Handler { + typealias Output = OperationOutput + typealias Context = HttpContext + typealias MockHandlerCallback = (Context, I) async throws -> Void + + private let handleCallback: MockHandlerCallback + + init(handleCallback: @escaping MockHandlerCallback) { + self.handleCallback = handleCallback + } + + func handle(context: Context, input: I) async throws -> Output { + try await handleCallback(context, input) + return OperationOutput(httpResponse: HttpResponse()) + } +} diff --git a/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift new file mode 100644 index 000000000..e2b48fe23 --- /dev/null +++ b/Tests/ClientRuntimeTests/NetworkingTests/ContentLengthMiddlewareTests.swift @@ -0,0 +1,92 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +import XCTest +import SmithyTestUtil +@testable import ClientRuntime + +class ContentLengthMiddlewareTests: XCTestCase { + private var builtContext: HttpContext! + private var stack: OperationStack! + + override func setUpWithError() throws { + try super.setUpWithError() + builtContext = HttpContextBuilder() + .withMethod(value: .get) + .withPath(value: "/") + .withEncoder(value: JSONEncoder()) + .withDecoder(value: JSONDecoder()) + .withOperation(value: "Test Operation") + .build() + stack = OperationStack(id: "Test Operation") + } + + func testTransferEncodingChunkedSetWhenStreamLengthIsNil() async throws { + addContentLengthMiddlewareWith(requiresLength: false, unsignedPayload: true) + forceEmptyStream() + try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "Chunked"]) + } + + func testTransferEncodingChunkedSetWithNilTraits() async throws { + // default constructor + addContentLengthMiddlewareWith(requiresLength: nil, unsignedPayload: nil) + forceEmptyStream() + try await AssertHeadersArePresent(expectedHeaders: ["Transfer-Encoding": "Chunked"]) + } + + func testContentLengthSetWhenStreamLengthAvailableAndRequiresLengthSet() async throws { + addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: false) + try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) + } + + func testContentLengthSetWhenRequiresLengthAndUnsignedPayload() async throws { + addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: true) + try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) + } + + func testRequiresLengthSetWithNilStreamShouldThrowError() async throws { + addContentLengthMiddlewareWith(requiresLength: true, unsignedPayload: false) + forceEmptyStream() + do { + try await AssertHeadersArePresent(expectedHeaders: ["Content-Length": "0"]) + XCTFail("Should throw error") + } catch let error as StreamError { + switch error { + case .notSupported("Missing content-length for SigV4 signing on operation: Test Operation"), .notSupported("Missing content-length for operation: Test Operation"): + // The error matches one of the expected cases, test passes + break + default: + XCTFail("Error is not StreamError.notSupported with expected message") + } + } + } + + private func addContentLengthMiddlewareWith(requiresLength: Bool?, unsignedPayload: Bool?) { + stack.finalizeStep.intercept( + position: .before, + middleware: ContentLengthMiddleware(requiresLength: requiresLength, unsignedPayload: unsignedPayload) + ) + } + + private func forceEmptyStream() { + // Force stream length to be nil + stack.finalizeStep.intercept(position: .before, id: "set nil stream length") { (context, input, next) -> OperationOutput in + input.body = .stream(BufferedStream()) // Set the stream length to nil + return try await next.handle(context: context, input: input) + } + } + + private func AssertHeadersArePresent(expectedHeaders: [String: String], file: StaticString = #file, line: UInt = #line) async throws -> Void { + let mockHandler = MockHandler { (_, input) in + for (key, value) in expectedHeaders { + XCTAssert(input.headers.value(for: key) == value, file: file, line: line) + } + let httpResponse = HttpResponse(body: HttpBody.none, statusCode: HttpStatusCode.ok) + let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) + let output = OperationOutput(httpResponse: httpResponse, output: mockOutput) + return output + } + + _ = try await stack.handleMiddleware(context: builtContext, input: MockInput(), next: mockHandler) + } +} \ No newline at end of file diff --git a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift index 2c07eb4b0..c1bc09257 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift @@ -7,6 +7,12 @@ import XCTest import AwsCommonRuntimeKit import struct Foundation.URLQueryItem @testable import ClientRuntime +// In Linux, Foundation.URLRequest is moved to FoundationNetworking. +#if canImport(FoundationNetworking) +import FoundationNetworking +#else +import struct Foundation.URLRequest +#endif class HttpRequestTests: NetworkingTestUtils { @@ -51,6 +57,30 @@ class HttpRequestTests: NetworkingTestUtils { } } + func testSdkHttpRequestToURLRequest() async throws { + let headers = Headers(["Testname-1": "testvalue-1", "Testname-2": "testvalue-2"]) + let endpoint = Endpoint(host: "host.com", path: "/", headers: headers) + + let httpBody = HttpBody.data(expectedMockRequestData) + let mockHttpRequest = SdkHttpRequest(method: .get, endpoint: endpoint, body: httpBody) + let urlRequest = try await URLRequest(sdkRequest: mockHttpRequest) + + XCTAssertNotNil(urlRequest) + guard let headersFromRequest = urlRequest.allHTTPHeaderFields else { + XCTFail("Headers in SdkHttpRequest were not successfully converted to headers in URLRequest.") + // Compiler doesn't recognize XCTFail as return / exception thrown + return + } + + // Check URLRequest fields + XCTAssertTrue(headersFromRequest.contains { $0.key == "Testname-1" && $0.value == "testvalue-1" }) + XCTAssertTrue(headersFromRequest.contains { $0.key == "Testname-2" && $0.value == "testvalue-2" }) + let expectedBody = try await httpBody.readData() + XCTAssertEqual(urlRequest.httpBody, expectedBody) + XCTAssertEqual(urlRequest.url, endpoint.url) + XCTAssertEqual(urlRequest.httpMethod, mockHttpRequest.method.rawValue) + } + func testCRTHeadersToSdkHeaders() throws { let builder = SdkHttpRequestBuilder() .withHeader(name: "Host", value: "amazon.aws.com") @@ -111,7 +141,9 @@ class HttpRequestTests: NetworkingTestUtils { } func testConversionToUrlRequestFailsWithInvalidEndpoint() { - // TODO:: When is the endpoint invalid or endpoint.url nil? - _ = Endpoint(host: "", path: "", protocolType: nil) + // Testing with an invalid endpoint where host is empty, + // path is empty, and protocolType is nil. + let endpoint = Endpoint(host: "", path: "", protocolType: nil) + XCTAssertNil(endpoint.url, "An invalid endpoint should result in a nil URL.") } } diff --git a/scripts/ci_steps/select_dependency_branch.sh b/scripts/ci_steps/select_dependency_branch.sh new file mode 100755 index 000000000..eb1d534bb --- /dev/null +++ b/scripts/ci_steps/select_dependency_branch.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + +# This script selects the Git reference for a locally installed dependency of another "original" repo. +# It matches the original's checked-out branch name if available, and if not, falls back to main. + +# Checks to see if Git repository at DEPENDENCY_REPO_URL has a branch named ORIGINAL_REPO_HEAD_REF. +# If so, the latest SHA for that branch is obtained and stored as DEPENDENCY_REPO_SHA in the Github env. +# +# If a branch named ORIGINAL_REPO_HEAD_REF does not exist in the Git repository at DEPENDENCY_REPO_URL, +# the SHA for the main branch is obtained and stored as DEPENDENCY_REPO_SHA in the Github env. + +# Parameters: +# ORIGINAL_REPO_HEAD_REF : the branch or tag name for the original repo branch being built. +# DEPENDENCY_REPO_URL : the URL to the dependency that will be matched to the branch + +# Output: +# DEPENDENCY_REPO_SHA : the Git SHA for the dependency repo commit to be built (set in the Github environment) + +echo "Finding correct branch for dependency repo: $DEPENDENCY_REPO_URL" +DEPENDENCY_BRANCH_SHA=`git ls-remote --heads "$DEPENDENCY_REPO_URL" "refs/heads/$ORIGINAL_REPO_HEAD_REF" | awk '{print $1}'` +if [[ ! -z "${DEPENDENCY_BRANCH_SHA}" ]]; then + echo "Ref $ORIGINAL_REPO_HEAD_REF was found on dependency repo at SHA $DEPENDENCY_BRANCH_SHA" + echo "Selecting dependency repo branch $ORIGINAL_REPO_HEAD_REF at $DEPENDENCY_BRANCH_SHA" + echo "DEPENDENCY_REPO_SHA=$DEPENDENCY_BRANCH_SHA" >> "$GITHUB_ENV" +else + echo "Ref $ORIGINAL_REPO_HEAD_REF was not found on dependency repo at SHA $DEPENDENCY_BRANCH_SHA" + DEPENDENCY_MAIN_SHA=`git ls-remote --heads "$ORIGINAL_REPO_HEAD_REF" "refs/heads/main" | awk '{print $1}'` + echo "Selecting dependency repo main branch at $DEPENDENCY_MAIN_SHA" + echo "DEPENDENCY_REPO_SHA=$DEPENDENCY_MAIN_SHA" >> "$GITHUB_ENV" +fi + 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 79f21a629..7e9ec15ee 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ClientRuntimeTypes.kt @@ -75,6 +75,7 @@ object ClientRuntimeTypes { val HeaderMiddleware = runtimeSymbol("HeaderMiddleware") val SerializableBodyMiddleware = runtimeSymbol("SerializableBodyMiddleware") val RetryMiddleware = runtimeSymbol("RetryMiddleware") + val IdempotencyTokenMiddleware = runtimeSymbol("IdempotencyTokenMiddleware") val NoopHandler = runtimeSymbol("NoopHandler") val SigningMiddleware = runtimeSymbol("SignerMiddleware") val AuthSchemeMiddleware = runtimeSymbol("AuthSchemeMiddleware") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ImportDeclarations.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ImportDeclarations.kt index da05a1b2f..eb305b223 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ImportDeclarations.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ImportDeclarations.kt @@ -8,7 +8,12 @@ package software.amazon.smithy.swift.codegen class ImportDeclarations { private val imports = mutableSetOf() - fun addImport(packageName: String, isTestable: Boolean = false, internalSPIName: String? = null) { + fun addImport( + packageName: String, + isTestable: Boolean = false, + internalSPIName: String? = null, + importOnlyIfCanImport: Boolean = false + ) { val existingImport = imports.find { it.packageName == packageName } if (existingImport != null) { // Update isTestable to true if needed @@ -17,10 +22,12 @@ class ImportDeclarations { if (internalSPIName != null) { existingImport.internalSPINames.add(internalSPIName) } + // Update importOnlyIfCanImport to true if needed + existingImport.importOnlyIfCanImport = existingImport.importOnlyIfCanImport || importOnlyIfCanImport } else { // Otherwise, we have a new package to import, so add it val internalSPINames = listOf(internalSPIName).mapNotNull { it }.toMutableSet() - imports.add(ImportStatement(packageName, isTestable, internalSPINames)) + imports.add(ImportStatement(packageName, isTestable, internalSPINames, importOnlyIfCanImport)) } } @@ -36,7 +43,12 @@ class ImportDeclarations { } } -private data class ImportStatement(val packageName: String, var isTestable: Boolean, val internalSPINames: MutableSet) { +private data class ImportStatement( + val packageName: String, + var isTestable: Boolean, + val internalSPINames: MutableSet, + var importOnlyIfCanImport: Boolean +) { val statement: String get() { var import = "import $packageName" @@ -46,6 +58,14 @@ private data class ImportStatement(val packageName: String, var isTestable: Bool if (isTestable) { import = "@testable $import" } + if (importOnlyIfCanImport) { + import = + """ + #if canImport($packageName) + $import + #endif + """.trimIndent() + } return import } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt index 56f099200..232a517f9 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/SwiftWriter.kt @@ -101,8 +101,13 @@ class SwiftWriter(private val fullPackageName: String) : CodeWriter() { } } - fun addImport(packageName: String, isTestable: Boolean = false, internalSPIName: String? = null) { - imports.addImport(packageName, isTestable, internalSPIName) + fun addImport( + packageName: String, + isTestable: Boolean = false, + internalSPIName: String? = null, + importOnlyIfCanImport: Boolean = false + ) { + imports.addImport(packageName, isTestable, internalSPIName, importOnlyIfCanImport) } fun addFoundationImport() { 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 ec4ad1b88..9c3b72311 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpBindingProtocolGenerator.kt @@ -4,6 +4,7 @@ */ package software.amazon.smithy.swift.codegen.integration +import software.amazon.smithy.aws.traits.auth.UnsignedPayloadTrait import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.model.knowledge.HttpBinding import software.amazon.smithy.model.knowledge.HttpBindingIndex @@ -30,6 +31,7 @@ import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait import software.amazon.smithy.model.traits.HttpQueryParamsTrait import software.amazon.smithy.model.traits.HttpQueryTrait import software.amazon.smithy.model.traits.MediaTypeTrait +import software.amazon.smithy.model.traits.RequiresLengthTrait import software.amazon.smithy.model.traits.StreamingTrait import software.amazon.smithy.model.traits.TimestampFormatTrait import software.amazon.smithy.swift.codegen.ClientRuntimeTypes @@ -62,6 +64,7 @@ import software.amazon.smithy.swift.codegen.integration.serde.UnionEncodeGenerat import software.amazon.smithy.swift.codegen.middleware.OperationMiddlewareGenerator import software.amazon.smithy.swift.codegen.model.ShapeMetadata import software.amazon.smithy.swift.codegen.model.bodySymbol +import software.amazon.smithy.swift.codegen.model.findStreamingMember import software.amazon.smithy.swift.codegen.model.hasEventStreamMember import software.amazon.smithy.swift.codegen.model.hasTrait import software.amazon.smithy.utils.OptionalUtils @@ -93,9 +96,8 @@ fun formatHeaderOrQueryValue( memberShape: MemberShape, location: HttpBinding.Location, bindingIndex: HttpBindingIndex, - defaultTimestampFormat: TimestampFormatTrait.Format + defaultTimestampFormat: TimestampFormatTrait.Format, ): Pair { - return when (val shape = ctx.model.expectShape(memberShape.target)) { is TimestampShape -> { val timestampFormat = bindingIndex.determineTimestampFormat(memberShape, location, defaultTimestampFormat) @@ -167,7 +169,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { writer.openBlock( "extension $symbolName: \$N {", "}", - SwiftTypes.Protocols.Encodable + SwiftTypes.Protocols.Encodable, ) { writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) @@ -288,7 +290,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { private fun generateCodingKeysForMembers( ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, - members: List + members: List, ) { codingKeysGenerator.generateCodingKeysForMembers(ctx, writer, members) } @@ -300,7 +302,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { val inputType = ctx.model.expectShape(operation.input.get()) var metadata = mapOf( Pair(ShapeMetadata.OPERATION_SHAPE, operation), - Pair(ShapeMetadata.SERVICE_VERSION, ctx.service.version) + Pair(ShapeMetadata.SERVICE_VERSION, ctx.service.version), ) shapesInfo.put(inputType, metadata) } @@ -338,7 +340,6 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { } private fun resolveShapesNeedingCodableConformance(ctx: ProtocolGenerator.GenerationContext): Set { - val topLevelOutputMembers = getHttpBindingOperations(ctx).flatMap { val outputShape = ctx.model.expectShape(it.output.get()) outputShape.members() @@ -392,7 +393,8 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { RelationshipType.LIST_MEMBER, RelationshipType.SET_MEMBER, RelationshipType.MAP_VALUE, - RelationshipType.UNION_MEMBER -> true + RelationshipType.UNION_MEMBER, + -> true else -> false } }.forEach { @@ -405,6 +407,29 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { return resolved } + // Checks for @requiresLength trait + // Returns true if the operation: + // - has a streaming member with @httpPayload trait + // - target is a blob shape with @requiresLength trait + private fun hasRequiresLengthTrait(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean { + if (op.input.isPresent) { + val inputShape = ctx.model.expectShape(op.input.get()) + val streamingMember = inputShape.findStreamingMember(ctx.model) + if (streamingMember != null) { + val targetShape = ctx.model.expectShape(streamingMember.target) + if (targetShape != null) { + return streamingMember.hasTrait() && + targetShape.isBlobShape && + targetShape.hasTrait() + } + } + } + return false + } + + // Checks for @unsignedPayload trait on an operation + private fun hasUnsignedPayloadTrait(op: OperationShape): Boolean = op.hasTrait() + override fun generateProtocolClient(ctx: ProtocolGenerator.GenerationContext) { val symbol = ctx.symbolProvider.toSymbol(ctx.service) ctx.delegator.useFileWriter("./${ctx.settings.moduleName}/${symbol.name}.swift") { writer -> @@ -416,7 +441,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { serviceSymbol.name, defaultContentType, httpProtocolCustomizable, - operationMiddleware + operationMiddleware, ) clientGenerator.render() } @@ -435,7 +460,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { operationMiddleware.appendMiddleware(operation, ContentTypeMiddleware(ctx.model, ctx.symbolProvider, resolver.determineRequestContentType(operation))) operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx.model, ctx.symbolProvider)) - operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, shouldRenderEncodableConformance)) + operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, shouldRenderEncodableConformance, hasRequiresLengthTrait(ctx, operation), hasUnsignedPayloadTrait(operation))) operationMiddleware.appendMiddleware(operation, DeserializeMiddleware(ctx.model, ctx.symbolProvider)) operationMiddleware.appendMiddleware(operation, LoggingMiddleware(ctx.model, ctx.symbolProvider)) @@ -469,7 +494,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { members: List, writer: SwiftWriter, defaultTimestampFormat: TimestampFormatTrait.Format, - path: String? = null + path: String? = null, ) protected abstract fun renderStructDecode( ctx: ProtocolGenerator.GenerationContext, @@ -477,7 +502,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { members: List, writer: SwiftWriter, defaultTimestampFormat: TimestampFormatTrait.Format, - path: String + path: String, ) protected abstract fun addProtocolSpecificMiddleware(ctx: ProtocolGenerator.GenerationContext, operation: OperationShape) @@ -493,11 +518,11 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator { for (operation in topDownIndex.getContainedOperations(ctx.service)) { OptionalUtils.ifPresentOrElse( Optional.of(getProtocolHttpBindingResolver(ctx, defaultContentType).httpTrait(operation)::class.java), - { containedOperations.add(operation) } + { containedOperations.add(operation) }, ) { LOGGER.warning( "Unable to fetch $protocolName protocol request bindings for ${operation.id} because " + - "it does not have an http binding trait" + "it does not have an http binding trait", ) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithoutHttpPayload.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithoutHttpPayload.kt index 8cf4024a0..cd76dd545 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithoutHttpPayload.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/bindingTraits/HttpResponseTraitWithoutHttpPayload.kt @@ -50,15 +50,18 @@ class HttpResponseTraitWithoutHttpPayload( .filter { !it.member.hasTrait(HttpQueryTrait::class.java) } .toMutableSet() val streamingMember = bodyMembers.firstOrNull { it.member.targetOrSelf(ctx.model).hasTrait(StreamingTrait::class.java) } - if (streamingMember != null) { - writeStreamingMember(streamingMember) + val initialResponseMembers = bodyMembers.filter { + val targetShape = it.member.targetOrSelf(ctx.model) + targetShape?.hasTrait(StreamingTrait::class.java) == false + }.toSet() + writeStreamingMember(streamingMember, initialResponseMembers) } else if (bodyMembersWithoutQueryTrait.isNotEmpty()) { writeNonStreamingMembers(bodyMembersWithoutQueryTrait) } } - fun writeStreamingMember(streamingMember: HttpBindingDescriptor) { + fun writeStreamingMember(streamingMember: HttpBindingDescriptor, initialResponseMembers: Set) { val shape = ctx.model.expectShape(streamingMember.member.target) val symbol = ctx.symbolProvider.toSymbol(shape) val memberName = ctx.symbolProvider.toMemberName(streamingMember.member) @@ -74,6 +77,9 @@ class HttpResponseTraitWithoutHttpPayload( symbol ) writer.write("self.\$L = decoderStream.toAsyncStream()", memberName) + if (isRPCService(ctx) && initialResponseMembers.isNotEmpty()) { + writeInitialResponseMembers(initialResponseMembers) + } } writer.indent() writer.write("self.\$L = nil", memberName).closeBlock("}") @@ -133,4 +139,52 @@ class HttpResponseTraitWithoutHttpPayload( } private val path: String = "properties.".takeIf { outputShape.hasTrait() } ?: "" + + private fun writeInitialResponseMembers(initialResponseMembers: Set) { + writer.apply { + write("if let initialDataWithoutHttp = await messageDecoder.awaitInitialResponse() {") + indent() + write("let decoder = JSONDecoder()") + write("do {") + indent() + write("let response = try decoder.decode([String: String].self, from: initialDataWithoutHttp)") + initialResponseMembers.forEach { responseMember -> + val responseMemberName = ctx.symbolProvider.toMemberName(responseMember.member) + write("self.$responseMemberName = response[\"$responseMemberName\"].map { value in KinesisClientTypes.Tag(value: value) }") + } + dedent() + write("} catch {") + indent() + write("print(\"Error decoding JSON: \\(error)\")") + initialResponseMembers.forEach { responseMember -> + val responseMemberName = ctx.symbolProvider.toMemberName(responseMember.member) + write("self.$responseMemberName = nil") + } + dedent() + write("}") + dedent() + write("} else {") + indent() + initialResponseMembers.forEach { responseMember -> + val responseMemberName = ctx.symbolProvider.toMemberName(responseMember.member) + write("self.$responseMemberName = nil") + } + dedent() + write("}") + } + } + + private fun isRPCService(ctx: ProtocolGenerator.GenerationContext): Boolean { + return rpcBoundProtocols.contains(ctx.protocol.name) + } + + /** + * A set of RPC-bound Smithy protocols + */ + private val rpcBoundProtocols = setOf( + "awsJson1_0", + "awsJson1_1", + "awsQuery", + "ec2Query", + ) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt index 22d70721f..af6975535 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentLengthMiddleware.kt @@ -9,7 +9,7 @@ import software.amazon.smithy.swift.codegen.middleware.MiddlewarePosition import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable import software.amazon.smithy.swift.codegen.middleware.MiddlewareStep -class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boolean) : MiddlewareRenderable { +class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boolean, private val requiresLength: Boolean, private val unsignedPayload: Boolean) : MiddlewareRenderable { override val name = "ContentLengthMiddleware" @@ -20,17 +20,17 @@ class ContentLengthMiddleware(val model: Model, private val alwaysIntercept: Boo override fun render( writer: SwiftWriter, op: OperationShape, - operationStackName: String + operationStackName: String, ) { val hasHttpBody = MiddlewareShapeUtils.hasHttpBody(model, op) if (hasHttpBody || alwaysIntercept) { - writer.write( - "\$L.\$L.intercept(position: \$L, middleware: \$N())", - operationStackName, - middlewareStep.stringValue(), - position.stringValue(), - ClientRuntimeTypes.Middleware.ContentLengthMiddleware - ) + val str = "requiresLength: $requiresLength, unsignedPayload: $unsignedPayload" + val middlewareArgs = str.takeIf { requiresLength || unsignedPayload } ?: "" + + val interceptStatement = "$operationStackName.${middlewareStep.stringValue()}.intercept(" + + "position: ${position.stringValue()}, middleware: ${ClientRuntimeTypes.Middleware.ContentLengthMiddleware}($middlewareArgs))" + + writer.write(interceptStatement) } } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt index 9f9963187..3fb6e4025 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/IdempotencyTokenMiddleware.kt @@ -24,25 +24,26 @@ class IdempotencyTokenMiddleware( override val name = "IdempotencyTokenMiddleware" override val middlewareStep = MiddlewareStep.INITIALIZESTEP override val position = MiddlewarePosition.AFTER - override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) { + override fun render(writer: SwiftWriter, op: OperationShape, operationStackName: String) { val inputShape = model.expectShape(op.input.get()) val idempotentMember = inputShape.members().firstOrNull { it.hasTrait() } idempotentMember?.let { val idempotentMemberName = it.memberName.decapitalize() + val inputShapeName = MiddlewareShapeUtils.inputSymbol(symbolProvider, model, op).name val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name val outputErrorShapeName = MiddlewareShapeUtils.outputErrorSymbolName(op) - writer.openBlock( - "$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, id: \"${name}\") { (context, input, next) -> \$N<$outputShapeName> in", "}", - ClientRuntimeTypes.Middleware.OperationOutput - ) { - writer.write("let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator()") - writer.write("var copiedInput = input") - writer.openBlock("if input.$idempotentMemberName == nil {", "}") { - writer.write("copiedInput.$idempotentMemberName = idempotencyTokenGenerator.generateToken()") - } - writer.write("return try await next.handle(context: context, input: copiedInput)") - } + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L, \$L>(keyPath: \\.\$L))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.IdempotencyTokenMiddleware, + inputShapeName, + outputShapeName, + outputErrorShapeName, + idempotentMemberName + ) } } } diff --git a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt index 1931499d8..412500644 100644 --- a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt @@ -29,14 +29,7 @@ class ContentMd5MiddlewareTests { .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) .build() var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") - operation.initializeStep.intercept(position: .after, id: "IdempotencyTokenMiddleware") { (context, input, next) -> ClientRuntime.OperationOutput in - let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator() - var copiedInput = input - if input.token == nil { - copiedInput.token = idempotencyTokenGenerator.generateToken() - } - return try await next.handle(context: context, input: copiedInput) - } + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.ContentMD5Middleware()) diff --git a/smithy-swift-codegen/src/test/kotlin/EventStreamsInitialResponseTests.kt b/smithy-swift-codegen/src/test/kotlin/EventStreamsInitialResponseTests.kt new file mode 100644 index 000000000..ad10072bd --- /dev/null +++ b/smithy-swift-codegen/src/test/kotlin/EventStreamsInitialResponseTests.kt @@ -0,0 +1,72 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import io.kotest.matchers.string.shouldContainOnlyOnce +import mocks.MockHttpAWSJson11ProtocolGenerator +import org.junit.jupiter.api.Test +import software.amazon.smithy.swift.codegen.integration.HttpBindingProtocolGenerator + +class EventStreamsInitialResponseTests { + @Test + fun `should attempt to decode response if initial-response members are present in RPC (awsJson) smithy model`() { + val context = setupInitialMessageTests( + "event-stream-initial-request-response.smithy", + "com.test#Example", + MockHttpAWSJson11ProtocolGenerator() + ) + val contents = getFileContents( + context.manifest, + "/InitialMessageEventStreams/models/TestStreamOperationWithInitialRequestResponseOutput+HttpResponseBinding.swift" + ) + contents.shouldSyntacticSanityCheck() + contents.shouldContainOnlyOnce( + """ + extension TestStreamOperationWithInitialRequestResponseOutput: ClientRuntime.HttpResponseBinding { + public init(httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder? = nil) async throws { + if case let .stream(stream) = httpResponse.body, let responseDecoder = decoder { + let messageDecoder: ClientRuntime.MessageDecoder? = nil + let decoderStream = ClientRuntime.EventStream.DefaultMessageDecoderStream(stream: stream, messageDecoder: messageDecoder, responseDecoder: responseDecoder) + self.value = decoderStream.toAsyncStream() + if let initialDataWithoutHttp = await messageDecoder.awaitInitialResponse() { + let decoder = JSONDecoder() + do { + let response = try decoder.decode([String: String].self, from: initialDataWithoutHttp) + self.initial1 = response["initial1"].map { value in KinesisClientTypes.Tag(value: value) } + self.initial2 = response["initial2"].map { value in KinesisClientTypes.Tag(value: value) } + } catch { + print("Error decoding JSON: \(error)") + self.initial1 = nil + self.initial2 = nil + } + } else { + self.initial1 = nil + self.initial2 = nil + } + } else { + self.value = nil + } + } + } + """.trimIndent() + ) + } + + private fun setupInitialMessageTests( + smithyFile: String, + serviceShapeId: String, + protocolGenerator: HttpBindingProtocolGenerator + ): TestContext { + val context = TestContext.initContextFrom(smithyFile, serviceShapeId, protocolGenerator) { model -> + model.defaultSettings(serviceShapeId, "InitialMessageEventStreams", "123", "InitialMessageEventStreams") + } + context.generator.initializeMiddleware(context.generationCtx) + context.generator.generateSerializers(context.generationCtx) + context.generator.generateProtocolClient(context.generationCtx) + context.generator.generateDeserializers(context.generationCtx) + context.generator.generateCodableConformanceForNestedTypes(context.generationCtx) + context.generationCtx.delegator.flushWriters() + return context + } +} diff --git a/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt index 46e608384..4f996a72b 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpBindingProtocolGeneratorTests.kt @@ -38,7 +38,7 @@ class TestHttpProtocolClientGeneratorFactory : HttpProtocolClientGeneratorFactor private fun getClientProperties(ctx: ProtocolGenerator.GenerationContext): List { return mutableListOf( DefaultRequestEncoder(), - DefaultResponseDecoder() + DefaultResponseDecoder(), ) } @@ -125,6 +125,7 @@ extension InlineDocumentAsPayloadOutput: ClientRuntime.HttpResponseBinding { """.trimIndent() contents.shouldContainOnlyOnce(expectedContents) } + @Test fun `default fooMap to an empty map if keysForFooMap is empty`() { val contents = getModelFileContents("example", "HttpPrefixHeadersOutput+HttpResponseBinding.swift", newTestContext.manifest) diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index fd8c8aaac..2bbf4a1d2 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -129,14 +129,7 @@ class HttpProtocolClientGeneratorTests { .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) .build() var operation = ClientRuntime.OperationStack(id: "allocateWidget") - operation.initializeStep.intercept(position: .after, id: "IdempotencyTokenMiddleware") { (context, input, next) -> ClientRuntime.OperationOutput in - let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator() - var copiedInput = input - if input.clientToken == nil { - copiedInput.clientToken = idempotencyTokenGenerator.generateToken() - } - return try await next.handle(context: context, input: copiedInput) - } + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.clientToken)) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) @@ -154,6 +147,123 @@ class HttpProtocolClientGeneratorTests { contents.shouldContainOnlyOnce(expected) } + @Test + fun `ContentLengthMiddleware generates correctly with requiresLength false and unsignedPayload true`() { + val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") + contents.shouldSyntacticSanityCheck() + val expected = + """ + public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStream") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .withAuthSchemes(value: config.authSchemes!) + .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) + .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() + contents.shouldContainOnlyOnce(expected) + } + + @Test + fun `ContentLengthMiddleware generates correctly with requiresLength true and unsignedPayload false`() { + val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") + contents.shouldSyntacticSanityCheck() + val expected = + """ + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .withAuthSchemes(value: config.authSchemes!) + .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) + .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() + contents.shouldContainOnlyOnce(expected) + } + + @Test + fun `ContentLengthMiddleware generates correctly with requiresLength true and unsignedPayload true`() { + val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") + val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") + contents.shouldSyntacticSanityCheck() + val expected = + """ + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput + { + let context = ClientRuntime.HttpContextBuilder() + .withEncoder(value: encoder) + .withDecoder(value: decoder) + .withMethod(value: .post) + .withServiceName(value: serviceName) + .withOperation(value: "unsignedFooBlobStreamWithLength") + .withIdempotencyTokenGenerator(value: config.idempotencyTokenGenerator) + .withLogger(value: config.logger) + .withPartitionID(value: config.partitionID) + .withAuthSchemes(value: config.authSchemes!) + .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) + .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() + contents.shouldContainOnlyOnce(expected) + } + private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { val context = TestContext.initContextFrom(smithyFile, serviceShapeId, MockHttpRestJsonProtocolGenerator()) { model -> model.defaultSettings(serviceShapeId, "RestJson", "2019-12-16", "Rest Json Protocol") diff --git a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt index 661568a78..381176188 100644 --- a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt @@ -29,14 +29,7 @@ class IdempotencyTokenTraitTests { .withAuthSchemeResolver(value: config.serviceSpecific.authSchemeResolver) .build() var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") - operation.initializeStep.intercept(position: .after, id: "IdempotencyTokenMiddleware") { (context, input, next) -> ClientRuntime.OperationOutput in - let idempotencyTokenGenerator = context.getIdempotencyTokenGenerator() - var copiedInput = input - if input.token == nil { - copiedInput.token = idempotencyTokenGenerator.generateToken() - } - return try await next.handle(context: context, input: copiedInput) - } + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) diff --git a/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpAWSJson11ProtocolGenerator.kt b/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpAWSJson11ProtocolGenerator.kt index 30f5c040a..04d70b2f4 100644 --- a/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpAWSJson11ProtocolGenerator.kt +++ b/smithy-swift-codegen/src/test/kotlin/mocks/MockHttpAWSJson11ProtocolGenerator.kt @@ -53,7 +53,8 @@ class MockAWSJson11HttpProtocolCustomizations() : DefaultHttpProtocolCustomizati writer: SwiftWriter, op: OperationShape, ) { - TODO("Not yet implemented") + // Not yet implemented + return } } diff --git a/smithy-swift-codegen/src/test/resources/event-stream-initial-request-response.smithy b/smithy-swift-codegen/src/test/resources/event-stream-initial-request-response.smithy new file mode 100644 index 000000000..417462aa9 --- /dev/null +++ b/smithy-swift-codegen/src/test/resources/event-stream-initial-request-response.smithy @@ -0,0 +1,39 @@ +namespace com.test + +use aws.protocols#awsJson1_1 +use aws.api#service +use aws.auth#sigv4 + +@awsJson1_1 +@sigv4(name: "event-stream-test") +@service(sdkId: "InitialMessageEventStreams") +service Example { + version: "123", + operations: [TestStreamOperationWithInitialRequestResponse] +} + +operation TestStreamOperationWithInitialRequestResponse { + input: TestStreamInputOutputInitialRequestResponse, + output: TestStreamInputOutputInitialRequestResponse, + errors: [SomeError], +} + +structure TestStreamInputOutputInitialRequestResponse { + @required + value: TestStream + initial1: String + initial2: String +} + +@error("client") +structure SomeError { + Message: String, +} + +structure MessageWithString { @eventPayload data: String } + +@streaming +union TestStream { + MessageWithString: MessageWithString, + SomeError: SomeError, +} \ No newline at end of file diff --git a/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy b/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy index 537e5531c..f1a6f4e71 100644 --- a/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy +++ b/smithy-swift-codegen/src/test/resources/service-generator-test-operations.smithy @@ -2,6 +2,7 @@ $version: "1.0" namespace com.test use aws.protocols#awsJson1_1 +use aws.auth#unsignedPayload @awsJson1_1 service Example { @@ -15,7 +16,10 @@ service Example { GetFooStreamingOutputNoInput, GetFooStreamingInputNoOutput, AllocateWidget, - OperationWithDeprecatedTrait + OperationWithDeprecatedTrait, + UnsignedFooBlobStream, + UnsignedFooBlobStreamWithLength, + ExplicitBlobStreamWithLength ] } @@ -89,4 +93,34 @@ operation AllocateWidget { structure AllocateWidgetInput { @idempotencyToken clientToken: String +} + +// Stream must have a known size +@streaming +@requiresLength +blob BodyStreamWithLength + +@http(method: "POST", uri: "/explicit/blobstreamunsigned") +@unsignedPayload +operation UnsignedFooBlobStream { + input: GetFooStreamingRequest, + output: GetFooStreamingResponse +} + +@http(method: "POST", uri: "/explicit/blobstreamunsignedwithlength") +@unsignedPayload +operation UnsignedFooBlobStreamWithLength { + input: ExplicitBlobStreamWithLengthRequest, + output: GetFooStreamingResponse +} + +@http(method: "POST", uri: "/explicit/blobstreamwithlength") +operation ExplicitBlobStreamWithLength { + input: ExplicitBlobStreamWithLengthRequest, + output: GetFooStreamingResponse +} + +structure ExplicitBlobStreamWithLengthRequest { + @httpPayload + payload1: BodyStreamWithLength } \ No newline at end of file