From 6aa9ebb979aa91314d20e4207d06dde649c76aef Mon Sep 17 00:00:00 2001 From: David Yaffe Date: Fri, 10 Jan 2025 07:25:59 -0800 Subject: [PATCH] feat: Support RPCv2 CBOR wire protocol (#1850) --- ...CborValidateResponseHeaderMiddleware.swift | 47 ++++++++++ .../Middlewares/UserAgentMiddleware.swift | 3 +- .../Protocols/AWSJSON/AWSJSONError.swift | 2 +- .../Protocols/RpcV2Cbor/RpcV2CborError.swift | 65 ++++++++++++++ .../UserAgent/AWSUserAgentMetadata.swift | 6 +- .../UserAgent/BusinessMetrics.swift | 15 +++- .../UserAgent/BusinessMetricsTests.swift | 9 +- .../Documentation.docc/AWSSDKForSwift.md | 2 + codegen/Package.swift | 3 +- .../protocol-test-codegen/build.gradle.kts | 4 +- .../smithy-aws-swift-codegen/build.gradle.kts | 1 + .../AWSHTTPBindingProtocolGenerator.kt | 3 + .../aws/swift/codegen/AWSServiceUtils.kt | 13 +-- .../swift/codegen/AWSSmokeTestGenerator.kt | 8 +- .../smithy/aws/swift/codegen/AddProtocols.kt | 4 +- .../rpcv2cbor/RPCV2CBORHttpBindingResolver.kt | 34 +++++++ .../rpcv2cbor/RpcV2CborCustomizations.kt | 15 ++++ .../rpcv2cbor/RpcV2CborProtocolGenerator.kt | 88 +++++++++++++++++++ ...V2CborValidateResponseHeaderIntegration.kt | 39 ++++++++ .../swiftmodules/AWSClientRuntimeTypes.kt | 5 ++ .../swift/codegen/PresignerGeneratorTests.kt | 8 +- .../awsquery/AWSQueryOperationStackTest.kt | 2 +- 22 files changed, 350 insertions(+), 26 deletions(-) create mode 100644 Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/RpcV2CborValidateResponseHeaderMiddleware.swift create mode 100644 Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/RpcV2Cbor/RpcV2CborError.swift create mode 100644 codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RPCV2CBORHttpBindingResolver.kt create mode 100644 codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborCustomizations.kt create mode 100644 codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborProtocolGenerator.kt create mode 100644 codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborValidateResponseHeaderIntegration.kt diff --git a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/RpcV2CborValidateResponseHeaderMiddleware.swift b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/RpcV2CborValidateResponseHeaderMiddleware.swift new file mode 100644 index 00000000000..c1c1f664c2a --- /dev/null +++ b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/RpcV2CborValidateResponseHeaderMiddleware.swift @@ -0,0 +1,47 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import ClientRuntime +import SmithyHTTPAPI + +public struct CborValidateResponseHeaderMiddleware { + public let id: Swift.String = "CborValidateResponseHeaderMiddleware" + + public init() {} +} + +public enum ServiceResponseError: Error { + case missingHeader(String) + case badHeaderValue(String) +} + +extension CborValidateResponseHeaderMiddleware: Interceptor { + + public typealias InputType = Input + public typealias OutputType = Output + public typealias RequestType = HTTPRequest + public typealias ResponseType = HTTPResponse + + public func readBeforeDeserialization( + context: some BeforeDeserialization + ) async throws { + let response = context.getResponse() + let smithyProtocolHeader = response.headers.value(for: "smithy-protocol") + + guard let smithyProtocolHeader else { + throw ServiceResponseError.missingHeader( + "smithy-protocol header is missing from a response over RpcV2 Cbor!" + ) + } + + guard smithyProtocolHeader == "rpc-v2-cbor" else { + throw ServiceResponseError.badHeaderValue( + "smithy-protocol header is set to \(smithyProtocolHeader) instead of expected value rpc-v2-cbor" + ) + } + } +} diff --git a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/UserAgentMiddleware.swift b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/UserAgentMiddleware.swift index 21eb9de13a9..50ef6cce14a 100644 --- a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/UserAgentMiddleware.swift +++ b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Middlewares/UserAgentMiddleware.swift @@ -41,7 +41,8 @@ extension UserAgentMiddleware: Interceptor { serviceID: serviceID, version: version, config: UserAgentValuesFromConfig(config: config), - context: context.getAttributes() + context: context.getAttributes(), + headers: context.getRequest().headers ).userAgent let builder = context.getRequest().toBuilder() builder.withHeader(name: USER_AGENT, value: awsUserAgentString) diff --git a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/AWSJSON/AWSJSONError.swift b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/AWSJSON/AWSJSONError.swift index abbabeb24fd..f666894baa6 100644 --- a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/AWSJSON/AWSJSONError.swift +++ b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/AWSJSON/AWSJSONError.swift @@ -38,7 +38,7 @@ public struct AWSJSONError: BaseError { extension AWSJSONError { @_spi(SmithyReadWrite) - public static func makeQueryCompatibleAWSJsonError( + public static func makeQueryCompatibleError( httpResponse: HTTPResponse, responseReader: Reader, noErrorWrapping: Bool, diff --git a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/RpcV2Cbor/RpcV2CborError.swift b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/RpcV2Cbor/RpcV2CborError.swift new file mode 100644 index 00000000000..236e2524550 --- /dev/null +++ b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/Protocols/RpcV2Cbor/RpcV2CborError.swift @@ -0,0 +1,65 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import protocol ClientRuntime.BaseError +import enum ClientRuntime.BaseErrorDecodeError +import class SmithyHTTPAPI.HTTPResponse +@_spi(SmithyReadWrite) import class SmithyCBOR.Reader + +public struct RpcV2CborError: BaseError { + public let code: String + public let message: String? + public let requestID: String? + @_spi(SmithyReadWrite) public var errorBodyReader: Reader { responseReader } + + public let httpResponse: HTTPResponse + private let responseReader: Reader + + @_spi(SmithyReadWrite) + public init(httpResponse: HTTPResponse, responseReader: Reader, noErrorWrapping: Bool, code: String? = nil) throws { + switch responseReader.cborValue { + case .map(let errorDetails): + if case let .text(errorCode) = errorDetails["__type"] { + self.code = sanitizeErrorType(errorCode) + } else { + self.code = "UnknownError" + } + + if case let .text(errorMessage) = errorDetails["Message"] { + self.message = errorMessage + } else { + self.message = nil + } + default: + self.code = "UnknownError" + self.message = nil + } + + self.httpResponse = httpResponse + self.responseReader = responseReader + self.requestID = nil + } +} + +// support awsQueryCompatible trait +extension RpcV2CborError { + @_spi(SmithyReadWrite) + public static func makeQueryCompatibleError( + httpResponse: HTTPResponse, + responseReader: Reader, + noErrorWrapping: Bool, + errorDetails: String? + ) throws -> RpcV2CborError { + let errorCode = try AwsQueryCompatibleErrorDetails.parse(errorDetails).code + return try RpcV2CborError( + httpResponse: httpResponse, + responseReader: responseReader, + noErrorWrapping: noErrorWrapping, + code: errorCode + ) + } +} diff --git a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/AWSUserAgentMetadata.swift b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/AWSUserAgentMetadata.swift index 1a178ccc1f2..1b76f8f7458 100644 --- a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/AWSUserAgentMetadata.swift +++ b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/AWSUserAgentMetadata.swift @@ -7,6 +7,7 @@ import ClientRuntime import class Smithy.Context +import struct SmithyHTTPAPI.Headers public struct AWSUserAgentMetadata { let sdkMetadata: SDKMetadata @@ -73,7 +74,8 @@ public struct AWSUserAgentMetadata { serviceID: String, version: String, config: UserAgentValuesFromConfig, - context: Context + context: Context, + headers: Headers ) -> AWSUserAgentMetadata { let apiMetadata = APIMetadata(serviceID: serviceID, version: version) let sdkMetadata = SDKMetadata(version: apiMetadata.version) @@ -82,7 +84,7 @@ public struct AWSUserAgentMetadata { let osVersion = PlatformOperationSystemVersion.operatingSystemVersion() let osMetadata = OSMetadata(family: currentOS, version: osVersion) let languageMetadata = LanguageMetadata(version: swiftVersion) - let businessMetrics = BusinessMetrics(config: config, context: context) + let businessMetrics = BusinessMetrics(config: config, context: context, headers: headers) let appIDMetadata = AppIDMetadata(name: config.appID) let frameworkMetadata = [FrameworkMetadata]() return AWSUserAgentMetadata( diff --git a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/BusinessMetrics.swift b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/BusinessMetrics.swift index 58b19e63a92..b4d7118c80d 100644 --- a/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/BusinessMetrics.swift +++ b/Sources/Core/AWSClientRuntime/Sources/AWSClientRuntime/UserAgent/BusinessMetrics.swift @@ -8,6 +8,7 @@ import ClientRuntime import class Smithy.Context import struct Smithy.AttributeKey +import struct SmithyHTTPAPI.Headers struct BusinessMetrics { // Mapping of human readable feature ID to the corresponding metric value @@ -15,9 +16,10 @@ struct BusinessMetrics { init( config: UserAgentValuesFromConfig, - context: Context + context: Context, + headers: Headers ) { - setFlagsIntoContext(config: config, context: context) + setFlagsIntoContext(config: config, context: context, headers: headers) self.features = context.businessMetrics } } @@ -73,7 +75,7 @@ public let businessMetricsKey = AttributeKey>(name: " "S3_EXPRESS_BUCKET" : "J" : "S3_ACCESS_GRANTS" : "K" : "GZIP_REQUEST_COMPRESSION" : "L" : - "PROTOCOL_RPC_V2_CBOR" : "M" : + "PROTOCOL_RPC_V2_CBOR" : "M" : Y "ENDPOINT_OVERRIDE" : "N" : Y "ACCOUNT_ID_ENDPOINT" : "O" : "ACCOUNT_ID_MODE_PREFERRED" : "P" : @@ -84,7 +86,8 @@ public let businessMetricsKey = AttributeKey>(name: " */ private func setFlagsIntoContext( config: UserAgentValuesFromConfig, - context: Context + context: Context, + headers: Headers ) { // Handle D, E, F switch config.awsRetryMode { @@ -103,4 +106,8 @@ private func setFlagsIntoContext( if context.selectedAuthScheme?.schemeID == "aws.auth#sigv4a" { context.businessMetrics = ["SIGV4A_SIGNING": "S"] } + // Handle M + if headers.value(for: "smithy-protocol") == "rpc-v2-cbor" { + context.businessMetrics = ["PROTOCOL_RPC_V2_CBOR": "M"] + } } diff --git a/Sources/Core/AWSClientRuntime/Tests/AWSClientRuntimeTests/UserAgent/BusinessMetricsTests.swift b/Sources/Core/AWSClientRuntime/Tests/AWSClientRuntimeTests/UserAgent/BusinessMetricsTests.swift index 672db12cadb..7728e718ccd 100644 --- a/Sources/Core/AWSClientRuntime/Tests/AWSClientRuntimeTests/UserAgent/BusinessMetricsTests.swift +++ b/Sources/Core/AWSClientRuntime/Tests/AWSClientRuntimeTests/UserAgent/BusinessMetricsTests.swift @@ -10,15 +10,18 @@ import ClientRuntime @testable import AWSClientRuntime import SmithyRetriesAPI import SmithyHTTPAuthAPI +import SmithyHTTPAPI import SmithyIdentity import SmithyRetriesAPI import Smithy class BusinessMetricsTests: XCTestCase { var context: Context! + var headers: Headers! override func setUp() async throws { context = Context(attributes: Attributes()) + headers = Headers() } func test_business_metrics_section_truncation() { @@ -29,7 +32,8 @@ class BusinessMetricsTests: XCTestCase { serviceID: "test", version: "1.0", config: UserAgentValuesFromConfig(appID: nil, endpoint: nil, awsRetryMode: .standard), - context: context + context: context, + headers: headers ) // Assert values in context match with values assigned to user agent XCTAssertEqual(userAgent.businessMetrics?.features, context.businessMetrics) @@ -51,7 +55,8 @@ class BusinessMetricsTests: XCTestCase { serviceID: "test", version: "1.0", config: UserAgentValuesFromConfig(appID: nil, endpoint: "test-endpoint", awsRetryMode: .adaptive), - context: context + context: context, + headers: headers ) // F comes from retry mode being adaptive & N comes from endpoint override let expectedString = "m/A,B,F,N,S" diff --git a/Sources/Core/AWSSDKForSwift/Documentation.docc/AWSSDKForSwift.md b/Sources/Core/AWSSDKForSwift/Documentation.docc/AWSSDKForSwift.md index 6fc58ac613e..2857ce06320 100644 --- a/Sources/Core/AWSSDKForSwift/Documentation.docc/AWSSDKForSwift.md +++ b/Sources/Core/AWSSDKForSwift/Documentation.docc/AWSSDKForSwift.md @@ -13,6 +13,8 @@ This SDK is open-source. Code is available on Github [here](https://github.com/ [Smithy](../../../../../swift/api/smithy/latest) +[SmithyCBOR](../../../../../swift/api/smithycbor/latest) + [SmithyChecksums](../../../../../swift/api/smithychecksums/latest) [SmithyChecksumsAPI](../../../../../swift/api/smithychecksumsapi/latest) diff --git a/codegen/Package.swift b/codegen/Package.swift index 8ecff3b4dfc..054b671dba6 100644 --- a/codegen/Package.swift +++ b/codegen/Package.swift @@ -93,7 +93,8 @@ private var protocolTestTargets: [Target] { .init(name: "EventStream", sourcePath: "\(baseDirLocal)/EventStream", buildOnly: true), .init(name: "RPCEventStream", sourcePath: "\(baseDirLocal)/RPCEventStream", buildOnly: true), .init(name: "Waiters", sourcePath: "\(baseDirLocal)/Waiters", testPath: "../codegen/protocol-test-codegen-local/Tests"), - .init(name: "StringArrayEndpointParam", sourcePath: "\(baseDirLocal)/StringArrayEndpointParam") + .init(name: "StringArrayEndpointParam", sourcePath: "\(baseDirLocal)/StringArrayEndpointParam"), + .init(name: "RPCV2CBORTestSDK", sourcePath: "\(baseDir)/smithy-rpcv2-cbor") ] return protocolTests.flatMap { protocolTest in let target = Target.target( diff --git a/codegen/protocol-test-codegen/build.gradle.kts b/codegen/protocol-test-codegen/build.gradle.kts index 9191df855b3..7684ace4274 100644 --- a/codegen/protocol-test-codegen/build.gradle.kts +++ b/codegen/protocol-test-codegen/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation("software.amazon.smithy:smithy-aws-protocol-tests:$smithyVersion") implementation("software.amazon.smithy:smithy-aws-traits:$smithyVersion") implementation(project(":smithy-aws-swift-codegen")) + implementation("software.amazon.smithy:smithy-protocol-tests:$smithyVersion") } val enabledProtocols = listOf( @@ -39,7 +40,8 @@ val enabledProtocols = listOf( ProtocolTest("apigateway", "com.amazonaws.apigateway#BackplaneControlService", "APIGatewayTestSDK"), ProtocolTest("glacier", "com.amazonaws.glacier#Glacier", "GlacierTestSDK"), ProtocolTest("s3", "com.amazonaws.s3#AmazonS3", "S3TestSDK"), - ProtocolTest("machinelearning", "com.amazonaws.machinelearning#AmazonML_20141212", "MachineLearningTestSDK") + ProtocolTest("machinelearning", "com.amazonaws.machinelearning#AmazonML_20141212", "MachineLearningTestSDK"), + ProtocolTest("smithy-rpcv2-cbor", "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol", "RPCV2CBORTestSDK"), ) // This project doesn't produce a JAR. diff --git a/codegen/smithy-aws-swift-codegen/build.gradle.kts b/codegen/smithy-aws-swift-codegen/build.gradle.kts index 316573c974e..a68688a99ce 100644 --- a/codegen/smithy-aws-swift-codegen/build.gradle.kts +++ b/codegen/smithy-aws-swift-codegen/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { api("software.amazon.smithy:smithy-aws-iam-traits:$smithyVersion") api("software.amazon.smithy:smithy-aws-cloudformation-traits:$smithyVersion") implementation("software.amazon.smithy:smithy-protocol-test-traits:$smithyVersion") + implementation("software.amazon.smithy:smithy-protocol-traits:$smithyVersion") testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion") testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion") implementation("software.amazon.smithy:smithy-rules-engine:$smithyVersion") diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSHTTPBindingProtocolGenerator.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSHTTPBindingProtocolGenerator.kt index 73764d6ca54..81f2c538ece 100644 --- a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSHTTPBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSHTTPBindingProtocolGenerator.kt @@ -80,6 +80,9 @@ abstract class AWSHTTPBindingProtocolGenerator( operation, OperationEndpointResolverMiddleware(ctx, customizations.endpointMiddlewareSymbol) ) + } + + override fun addUserAgentMiddleware(ctx: ProtocolGenerator.GenerationContext, operation: OperationShape) { operationMiddleware.appendMiddleware(operation, UserAgentMiddleware(ctx.settings)) } } diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSServiceUtils.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSServiceUtils.kt index a0c6997d0f9..7e75cb4e4e7 100644 --- a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSServiceUtils.kt +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSServiceUtils.kt @@ -7,24 +7,25 @@ package software.amazon.smithy.aws.swift.codegen import software.amazon.smithy.aws.traits.ServiceTrait import software.amazon.smithy.model.shapes.ServiceShape -import software.amazon.smithy.swift.codegen.model.expectTrait +import software.amazon.smithy.swift.codegen.model.getTrait /** * Get the [sdkId](https://smithy.io/2.0/aws/aws-core.html#sdkid) from the (AWS) service shape + * or return a default value if the trait is not present. */ val ServiceShape.sdkId: String - get() = expectTrait().sdkId + get() = getTrait()?.sdkId ?: "defaultSdkId" /** * Get the [arnNamespace](https://smithy.io/2.0/aws/aws-core.html#arnnamespace) - * from the (AWS) service shape + * from the (AWS) service shape or return a default value if the trait is not present. */ val ServiceShape.arnNamespace: String - get() = expectTrait().arnNamespace + get() = getTrait()?.arnNamespace ?: "defaultArnNamespace" /** * Get the [endpointPrefix](https://smithy.io/2.0/aws/aws-core.html#endpointprefix) - * from the (AWS) service shape + * from the (AWS) service shape or return a default value if the trait is not present. */ val ServiceShape.endpointPrefix: String - get() = expectTrait().endpointPrefix + get() = getTrait()?.endpointPrefix ?: "defaultEndpointPrefix" diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSSmokeTestGenerator.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSSmokeTestGenerator.kt index 0a04ffa3bec..3164d096e08 100644 --- a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSSmokeTestGenerator.kt +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AWSSmokeTestGenerator.kt @@ -26,11 +26,15 @@ class AWSSmokeTestGenerator( ) override fun getServiceName(): String { - return "AWS" + ctx.service.getTrait(ServiceTrait::class.java).get().sdkId.toUpperCamelCase() + val serviceTrait = ctx.service.getTrait(ServiceTrait::class.java).orElse(null) + val sdkId = serviceTrait?.sdkId?.toUpperCamelCase() ?: "DefaultService" + return "AWS$sdkId" } override fun getClientName(): String { - return ctx.service.getTrait(ServiceTrait::class.java).get().sdkId.toUpperCamelCase().removeSuffix("Service") + "Client" + val serviceTrait = ctx.service.getTrait(ServiceTrait::class.java).orElse(null) + val sdkId = serviceTrait?.sdkId?.toUpperCamelCase()?.removeSuffix("Service") ?: "Default" + return "${sdkId}Client" } override fun renderCustomFilePrivateVariables(writer: SwiftWriter) { diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AddProtocols.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AddProtocols.kt index c07bf2f1996..ff38ac74c3d 100644 --- a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AddProtocols.kt +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/AddProtocols.kt @@ -10,6 +10,7 @@ import software.amazon.smithy.aws.swift.codegen.protocols.awsquery.AWSQueryProto import software.amazon.smithy.aws.swift.codegen.protocols.ec2query.EC2QueryProtocolGenerator import software.amazon.smithy.aws.swift.codegen.protocols.restjson.AWSRestJson1ProtocolGenerator import software.amazon.smithy.aws.swift.codegen.protocols.restxml.RestXMLProtocolGenerator +import software.amazon.smithy.aws.swift.codegen.protocols.rpcv2cbor.RpcV2CborProtocolGenerator import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.SwiftIntegration @@ -31,6 +32,7 @@ class AddProtocols : SwiftIntegration { AWSJSON1_1ProtocolGenerator(), RestXMLProtocolGenerator(), AWSQueryProtocolGenerator(), - EC2QueryProtocolGenerator() + EC2QueryProtocolGenerator(), + RpcV2CborProtocolGenerator() ) } diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RPCV2CBORHttpBindingResolver.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RPCV2CBORHttpBindingResolver.kt new file mode 100644 index 00000000000..56de6a809bf --- /dev/null +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RPCV2CBORHttpBindingResolver.kt @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.aws.swift.codegen.protocols.rpcv2cbor + +import software.amazon.smithy.model.pattern.UriPattern +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.traits.HttpTrait +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.protocols.core.StaticHttpBindingResolver + +class RPCV2CBORHttpBindingResolver( + context: ProtocolGenerator.GenerationContext, + defaultContentType: String +) : StaticHttpBindingResolver(context, rpcv2CborHttpTrait, defaultContentType) { + + companion object { + private val rpcv2CborHttpTrait: HttpTrait = HttpTrait + .builder() + .code(200) + .method("POST") + .uri(UriPattern.parse("/")) + .build() + } + + override fun httpTrait(operationShape: OperationShape): HttpTrait = HttpTrait + .builder() + .code(200) + .method("POST") + .uri(UriPattern.parse("/service/$serviceName/operation/${operationShape.id.name}")) + .build() +} diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborCustomizations.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborCustomizations.kt new file mode 100644 index 00000000000..17a94d16902 --- /dev/null +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborCustomizations.kt @@ -0,0 +1,15 @@ +package software.amazon.smithy.aws.swift.codegen.protocols.rpcv2cbor + +import software.amazon.smithy.aws.swift.codegen.AWSHTTPProtocolCustomizations +import software.amazon.smithy.aws.swift.codegen.swiftmodules.AWSClientRuntimeTypes +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.traits.TimestampFormatTrait + +class RpcV2CborCustomizations : AWSHTTPProtocolCustomizations() { + + override val baseErrorSymbol: Symbol = AWSClientRuntimeTypes.RpcV2Cbor.RpcV2CborError + + // Timestamp format is not used in RpcV2Cbor since it's a binary protocol. We seem to be missing an abstraction + // between text-based and binary-based protocols + override val defaultTimestampFormat = TimestampFormatTrait.Format.UNKNOWN +} diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborProtocolGenerator.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborProtocolGenerator.kt new file mode 100644 index 00000000000..c3a53de0062 --- /dev/null +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborProtocolGenerator.kt @@ -0,0 +1,88 @@ +package software.amazon.smithy.aws.swift.codegen.protocols.rpcv2cbor + +import software.amazon.smithy.aws.swift.codegen.AWSHTTPBindingProtocolGenerator +import software.amazon.smithy.aws.swift.codegen.middleware.MutateHeadersMiddleware +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ShapeId +import software.amazon.smithy.model.traits.StreamingTrait +import software.amazon.smithy.model.traits.UnitTypeTrait +import software.amazon.smithy.protocol.traits.Rpcv2CborTrait +import software.amazon.smithy.swift.codegen.SyntheticClone +import software.amazon.smithy.swift.codegen.integration.HttpBindingResolver +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.middlewares.ContentLengthMiddleware +import software.amazon.smithy.swift.codegen.integration.middlewares.ContentTypeMiddleware +import software.amazon.smithy.swift.codegen.integration.middlewares.OperationInputBodyMiddleware +import software.amazon.smithy.swift.codegen.model.getTrait +import software.amazon.smithy.swift.codegen.model.hasTrait +import software.amazon.smithy.swift.codegen.model.targetOrSelf + +class RpcV2CborProtocolGenerator : AWSHTTPBindingProtocolGenerator(RpcV2CborCustomizations()) { + override val defaultContentType = "application/cbor" + override val protocol: ShapeId = Rpcv2CborTrait.ID + + override val shouldRenderEncodableConformance = true + + override fun getProtocolHttpBindingResolver(ctx: ProtocolGenerator.GenerationContext, defaultContentType: String): + HttpBindingResolver = RPCV2CBORHttpBindingResolver(ctx, defaultContentType) + + override fun addProtocolSpecificMiddleware(ctx: ProtocolGenerator.GenerationContext, operation: OperationShape) { + super.addProtocolSpecificMiddleware(ctx, operation) + + operationMiddleware.removeMiddleware(operation, "OperationInputBodyMiddleware") + operationMiddleware.appendMiddleware(operation, OperationInputBodyMiddleware(ctx.model, ctx.symbolProvider, true)) + + val hasEventStreamResponse = ctx.model.expectShape(operation.outputShape).hasTrait() + val hasEventStreamRequest = ctx.model.expectShape(operation.inputShape).hasTrait() + + // Determine the value of the Accept header based on output shape + val acceptHeaderValue = if (hasEventStreamResponse) { + "application/vnd.amazon.eventstream" + } else { + "application/cbor" + } + + // Determine the value of the Content-Type header based on input shape + val contentTypeValue = if (hasEventStreamRequest) { + "application/vnd.amazon.eventstream" + } else { + defaultContentType + } + + // Middleware to set smithy-protocol and Accept headers + // Every request for the rpcv2Cbor protocol MUST contain a smithy-protocol header with the value of rpc-v2-cbor + val smithyProtocolRequestHeaderMiddleware = MutateHeadersMiddleware( + overrideHeaders = mapOf( + "smithy-protocol" to "rpc-v2-cbor", + "Accept" to acceptHeaderValue + ) + ) + + operationMiddleware.appendMiddleware(operation, smithyProtocolRequestHeaderMiddleware) + operationMiddleware.appendMiddleware(operation, CborValidateResponseHeaderMiddleware) + + if (operation.hasHttpBody(ctx)) { + operationMiddleware.appendMiddleware(operation, ContentTypeMiddleware(ctx.model, ctx.symbolProvider, contentTypeValue, true)) + } + + // Only set Content-Length header if the request input shape doesn't have an event stream + if (!hasEventStreamRequest) { + operationMiddleware.appendMiddleware(operation, ContentLengthMiddleware(ctx.model, true, false, false)) + } + } + + /** + * @return whether the operation input does _not_ target the unit shape ([UnitTypeTrait.UNIT]) + */ + private fun OperationShape.hasHttpBody(ctx: ProtocolGenerator.GenerationContext): Boolean { + val input = ctx.model.expectShape(inputShape).targetOrSelf(ctx.model).let { + // If the input has been synthetically cloned from the original (most likely), + // pull the archetype and check _that_ + it.getTrait()?.let { clone -> + ctx.model.expectShape(clone.archetype).targetOrSelf(ctx.model) + } ?: it + } + + return input.id != UnitTypeTrait.UNIT + } +} diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborValidateResponseHeaderIntegration.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborValidateResponseHeaderIntegration.kt new file mode 100644 index 00000000000..c7f01b13165 --- /dev/null +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/protocols/rpcv2cbor/RpcV2CborValidateResponseHeaderIntegration.kt @@ -0,0 +1,39 @@ +package software.amazon.smithy.aws.swift.codegen.protocols.rpcv2cbor + +import software.amazon.smithy.aws.swift.codegen.swiftmodules.AWSClientRuntimeTypes +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.ServiceShape +import software.amazon.smithy.protocol.traits.Rpcv2CborTrait +import software.amazon.smithy.swift.codegen.SwiftSettings +import software.amazon.smithy.swift.codegen.SwiftWriter +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.SwiftIntegration +import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils +import software.amazon.smithy.swift.codegen.middleware.MiddlewareRenderable +import software.amazon.smithy.swift.codegen.model.shapes + +class CborValidateResponseHeaderIntegration : SwiftIntegration { + override fun enabledForService(model: Model, settings: SwiftSettings): Boolean = model + .shapes() + .any { it.hasTrait(Rpcv2CborTrait::class.java) } +} + +object CborValidateResponseHeaderMiddleware : MiddlewareRenderable { + override val name = "CborValidateResponseHeaderMiddleware" + + override fun renderMiddlewareInit( + ctx: ProtocolGenerator.GenerationContext, + writer: SwiftWriter, + op: OperationShape + ) { + val inputShapeName = MiddlewareShapeUtils.inputSymbol(ctx.symbolProvider, ctx.model, op).name + val outputShapeName = MiddlewareShapeUtils.outputSymbol(ctx.symbolProvider, ctx.model, op).name + writer.write( + "\$N<\$L, \$L>()", + AWSClientRuntimeTypes.RpcV2Cbor.CborValidateResponseHeaderMiddleware, + inputShapeName, + outputShapeName, + ) + } +} diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/swiftmodules/AWSClientRuntimeTypes.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/swiftmodules/AWSClientRuntimeTypes.kt index 1596ff8632b..b7ae7a89281 100644 --- a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/swiftmodules/AWSClientRuntimeTypes.kt +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/swiftmodules/AWSClientRuntimeTypes.kt @@ -29,6 +29,11 @@ object AWSClientRuntimeTypes { } } + object RpcV2Cbor { + val RpcV2CborError = runtimeSymbol("RpcV2CborError", SwiftDeclaration.STRUCT, listOf("SmithyReadWrite")) + val CborValidateResponseHeaderMiddleware = runtimeSymbol("CborValidateResponseHeaderMiddleware", SwiftDeclaration.STRUCT) + } + object Core { val AWSUserAgentMetadata = runtimeSymbol("AWSUserAgentMetadata", SwiftDeclaration.STRUCT) val UserAgentMiddleware = runtimeSymbol("UserAgentMiddleware", SwiftDeclaration.STRUCT) diff --git a/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/PresignerGeneratorTests.kt b/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/PresignerGeneratorTests.kt index f77543ec680..cf61617853c 100644 --- a/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/PresignerGeneratorTests.kt +++ b/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/PresignerGeneratorTests.kt @@ -55,8 +55,8 @@ extension GetFooInput { builder.applySigner(ClientRuntime.SignerMiddleware()) let endpointParams = EndpointParams() builder.applyEndpoint(AWSClientRuntime.EndpointResolverMiddleware(endpointResolverBlock: { [config] in try config.endpointResolver.resolve(params: ${'$'}0) }, endpointParams: endpointParams)) - builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: ExampleClient.version, config: config)) builder.selectAuthScheme(ClientRuntime.AuthSchemeMiddleware()) + builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: ExampleClient.version, config: config)) var metricsAttributes = Smithy.Attributes() metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.service, value: "Example") metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.method, value: "GetFoo") @@ -127,8 +127,8 @@ extension PostFooInput { builder.applySigner(ClientRuntime.SignerMiddleware()) let endpointParams = EndpointParams() builder.applyEndpoint(AWSClientRuntime.EndpointResolverMiddleware(endpointResolverBlock: { [config] in try config.endpointResolver.resolve(params: ${'$'}0) }, endpointParams: endpointParams)) - builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: ExampleClient.version, config: config)) builder.selectAuthScheme(ClientRuntime.AuthSchemeMiddleware()) + builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: ExampleClient.version, config: config)) var metricsAttributes = Smithy.Attributes() metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.service, value: "Example") metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.method, value: "PostFoo") @@ -199,8 +199,8 @@ extension PutFooInput { builder.applySigner(ClientRuntime.SignerMiddleware()) let endpointParams = EndpointParams() builder.applyEndpoint(AWSClientRuntime.EndpointResolverMiddleware(endpointResolverBlock: { [config] in try config.endpointResolver.resolve(params: ${'$'}0) }, endpointParams: endpointParams)) - builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: ExampleClient.version, config: config)) builder.selectAuthScheme(ClientRuntime.AuthSchemeMiddleware()) + builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: ExampleClient.version, config: config)) var metricsAttributes = Smithy.Attributes() metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.service, value: "Example") metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.method, value: "PutFoo") @@ -272,8 +272,8 @@ extension PutObjectInput { let endpointParams = EndpointParams() context.set(key: Smithy.AttributeKey(name: "EndpointParams"), value: endpointParams) builder.applyEndpoint(AWSClientRuntime.EndpointResolverMiddleware(endpointResolverBlock: { [config] in try config.endpointResolver.resolve(params: ${'$'}0) }, endpointParams: endpointParams)) - builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: S3Client.version, config: config)) builder.selectAuthScheme(ClientRuntime.AuthSchemeMiddleware()) + builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: S3Client.version, config: config)) var metricsAttributes = Smithy.Attributes() metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.service, value: "S3") metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.method, value: "PutObject") diff --git a/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/awsquery/AWSQueryOperationStackTest.kt b/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/awsquery/AWSQueryOperationStackTest.kt index 2eaff2bd73c..9cbab46b7a7 100644 --- a/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/awsquery/AWSQueryOperationStackTest.kt +++ b/codegen/smithy-aws-swift-codegen/src/test/kotlin/software/amazon/smithy/aws/swift/codegen/awsquery/AWSQueryOperationStackTest.kt @@ -38,12 +38,12 @@ class AWSQueryOperationStackTest { builder.applySigner(ClientRuntime.SignerMiddleware()) let endpointParams = EndpointParams() builder.applyEndpoint(AWSClientRuntime.EndpointResolverMiddleware(endpointResolverBlock: { [config] in try config.endpointResolver.resolve(params: ${'$'}0) }, endpointParams: endpointParams)) - builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: QueryProtocolClient.version, config: config)) builder.serialize(ClientRuntime.BodyMiddleware(rootNodeInfo: "", inputWritingClosure: NoInputAndOutputInput.write(value:to:))) builder.interceptors.add(ClientRuntime.ContentTypeMiddleware(contentType: "application/x-www-form-urlencoded")) builder.selectAuthScheme(ClientRuntime.AuthSchemeMiddleware()) builder.interceptors.add(AWSClientRuntime.AmzSdkInvocationIdMiddleware()) builder.interceptors.add(AWSClientRuntime.AmzSdkRequestMiddleware(maxRetries: config.retryStrategyOptions.maxRetriesBase)) + builder.interceptors.add(AWSClientRuntime.UserAgentMiddleware(serviceID: serviceName, version: QueryProtocolClient.version, config: config)) var metricsAttributes = Smithy.Attributes() metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.service, value: "QueryProtocol") metricsAttributes.set(key: ClientRuntime.OrchestratorMetricsAttributesKeys.method, value: "NoInputAndOutput")