-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Move RPCv2CBOR generation to smithy-swift #900
Changes from 36 commits
b236c1a
c6a872c
37fdc0e
cdbaadb
10dccdd
50ceb88
4eec99b
ebde137
6b1e91c
e587f61
7a321a0
3e1f884
079a6c3
b8475ce
36628b4
9b2880b
f14c31b
2601702
2713fbc
2de2171
2b3120e
5bc3d23
d7da330
6a607ae
dde4eba
40f271f
3accc45
0b9d049
cb26666
2a1ba81
da49a1f
9d64040
0a795b3
8dd5737
791cf36
7c90fc9
65898df
b29b7f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import SmithyIdentity | ||
import SmithyIdentityAPI | ||
import protocol SmithyHTTPAPI.HTTPClient | ||
import struct SmithyRetries.DefaultRetryStrategy | ||
import struct SmithyRetries.ExponentialBackoffStrategy | ||
import struct SmithyRetriesAPI.RetryStrategyOptions | ||
|
||
public typealias RuntimeConfigType | ||
= DefaultSDKRuntimeConfiguration<DefaultRetryStrategy, DefaultRetryErrorInfoProvider> | ||
|
||
open class ClientConfigDefaultsProvider { | ||
/// Returns a default `HTTPClient` engine. | ||
open class func httpClientEngine() -> HTTPClient { | ||
return RuntimeConfigType.makeClient( | ||
httpClientConfiguration: RuntimeConfigType.defaultHttpClientConfiguration | ||
) | ||
} | ||
|
||
/// Returns default `HttpClientConfiguration`. | ||
open class func httpClientConfiguration() -> HttpClientConfiguration { | ||
return RuntimeConfigType.defaultHttpClientConfiguration | ||
} | ||
|
||
/// Returns a default idempotency token generator. | ||
open class func idempotencyTokenGenerator() -> IdempotencyTokenGenerator { | ||
return RuntimeConfigType.defaultIdempotencyTokenGenerator | ||
} | ||
|
||
/// Returns a default client logging mode. | ||
open class func clientLogMode() -> ClientLogMode { | ||
return RuntimeConfigType.defaultClientLogMode | ||
} | ||
|
||
/// Returns default retry strategy options *without* referencing AWS-specific config. | ||
open class func retryStrategyOptions(maxAttempts: Int? = nil) -> RetryStrategyOptions { | ||
// Provide some simple fallback for non-AWS usage, e.g. a standard exponential backoff. | ||
let attempts = maxAttempts ?? 3 | ||
return RetryStrategyOptions( | ||
backoffStrategy: ExponentialBackoffStrategy(), | ||
maxRetriesBase: attempts - 1, | ||
rateLimitingMode: .standard | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,19 +16,17 @@ import enum SmithyHTTPAuthAPI.SigningPropertyKeys | |
public struct EndpointResolverMiddleware<OperationStackOutput, Params: EndpointsRequestContextProviding> { | ||
public let id: Swift.String = "EndpointResolverMiddleware" | ||
|
||
let endpointResolverBlock: (Params) throws -> Endpoint | ||
|
||
let endpointParams: Params | ||
|
||
let paramsBlock: (Context) throws -> Params | ||
let resolverBlock: (Params) throws -> Endpoint | ||
let authSchemeResolver: EndpointsAuthSchemeResolver | ||
|
||
public init( | ||
endpointResolverBlock: @escaping (Params) throws -> Endpoint, | ||
endpointParams: Params, | ||
paramsBlock: @escaping (Context) throws -> Params, | ||
resolverBlock: @escaping (Params) throws -> Endpoint, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This updates EndpointResolverMiddleware.swift in smithy-swift to include type updates made to the same class in AWSClientRuntime |
||
authSchemeResolver: EndpointsAuthSchemeResolver = DefaultEndpointsAuthSchemeResolver() | ||
) { | ||
self.endpointResolverBlock = endpointResolverBlock | ||
self.endpointParams = endpointParams | ||
self.paramsBlock = paramsBlock | ||
self.resolverBlock = resolverBlock | ||
self.authSchemeResolver = authSchemeResolver | ||
} | ||
} | ||
|
@@ -42,7 +40,7 @@ extension EndpointResolverMiddleware: ApplyEndpoint { | |
) async throws -> HTTPRequest { | ||
let builder = request.toBuilder() | ||
|
||
let endpoint = try endpointResolverBlock(endpointParams) | ||
let endpoint = try resolverBlock(paramsBlock(attributes)) | ||
|
||
var signingName: String? | ||
var signingAlgorithm: String? | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import class SmithyHTTPAPI.HTTPResponse | ||
@_spi(SmithyReadWrite) import class SmithyCBOR.Reader | ||
|
||
public struct RpcV2CborError: BaseError { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved from aws-sdk-swift |
||
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 | ||
} | ||
} | ||
|
||
/// Filter additional information from error name and sanitize it | ||
/// Reference: https://awslabs.github.io/smithy/1.0/spec/aws/aws-restjson1-protocol.html#operation-error-serialization | ||
func sanitizeErrorType(_ type: String) -> String { | ||
return type.substringAfter("#").substringBefore(":").trim() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// | ||
// Copyright Amazon.com Inc. or its affiliates. | ||
// All Rights Reserved. | ||
// | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// | ||
|
||
import SmithyHTTPAPI | ||
|
||
public struct CborValidateResponseHeaderMiddleware<Input, Output> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved from aws-sdk-swift |
||
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<InputType, RequestType, ResponseType> | ||
) 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" | ||
) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0. | ||
*/ | ||
|
||
package software.amazon.smithy.swift.codegen | ||
|
||
import software.amazon.smithy.codegen.core.CodegenException | ||
import software.amazon.smithy.model.node.Node | ||
import software.amazon.smithy.rulesengine.language.EndpointRuleSet | ||
import software.amazon.smithy.rulesengine.language.evaluation.value.ArrayValue | ||
import software.amazon.smithy.rulesengine.language.evaluation.value.BooleanValue | ||
import software.amazon.smithy.rulesengine.language.evaluation.value.EmptyValue | ||
import software.amazon.smithy.rulesengine.language.evaluation.value.IntegerValue | ||
import software.amazon.smithy.rulesengine.language.evaluation.value.RecordValue | ||
import software.amazon.smithy.rulesengine.language.evaluation.value.StringValue | ||
import software.amazon.smithy.rulesengine.language.evaluation.value.Value | ||
import software.amazon.smithy.rulesengine.traits.EndpointTestsTrait | ||
import software.amazon.smithy.swift.codegen.endpoints.EndpointTypes | ||
import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator | ||
import software.amazon.smithy.swift.codegen.swiftmodules.ClientRuntimeTypes | ||
import software.amazon.smithy.swift.codegen.swiftmodules.SmithyHTTPAPITypes | ||
import software.amazon.smithy.swift.codegen.swiftmodules.SmithyTestUtilTypes | ||
import software.amazon.smithy.swift.codegen.swiftmodules.XCTestTypes | ||
import software.amazon.smithy.swift.codegen.utils.toLowerCamelCase | ||
|
||
/** | ||
* Generates code for EndpointResolver tests. | ||
*/ | ||
class EndpointTestGenerator( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved from aws-sdk-swift as it is used by protocol generators |
||
private val endpointTest: EndpointTestsTrait, | ||
private val endpointRuleSet: EndpointRuleSet?, | ||
private val ctx: ProtocolGenerator.GenerationContext | ||
) { | ||
fun render(writer: SwiftWriter): Int { | ||
if (endpointTest.testCases.isEmpty()) { return 0 } | ||
|
||
writer.addImport(ctx.settings.moduleName, isTestable = true) | ||
writer.addImport(SwiftDependency.XCTest.target) | ||
|
||
// used to filter out test params that are not valid | ||
val endpointParamsMembers = endpointRuleSet?.parameters?.toList()?.map { it.name.name.value }?.toSet() ?: emptySet() | ||
|
||
var count = 0 | ||
writer.openBlock("class EndpointResolverTest: \$N {", "}", XCTestTypes.XCTestCase) { | ||
writer.write("") | ||
writer.openBlock("override class func setUp() {", "}") { | ||
writer.write("\$N.initialize()", SmithyTestUtilTypes.TestInitializer) | ||
} | ||
writer.write("") | ||
|
||
endpointTest.testCases.forEach { testCase -> | ||
writer.write("/// \$L", testCase.documentation) | ||
writer.openBlock("func testResolve${++count}() throws {", "}") { | ||
writer.openBlock("let endpointParams = \$N(", ")", EndpointTypes.EndpointParams) { | ||
val applicableParams = | ||
testCase.params.members.filter { endpointParamsMembers.contains(it.key.value) } | ||
.toSortedMap(compareBy { it.value }).map { (key, value) -> | ||
key to value | ||
} | ||
|
||
applicableParams.forEachIndexed { idx, pair -> | ||
writer.writeInline("${pair.first.value.toLowerCamelCase()}: ") | ||
val value = Value.fromNode(pair.second) | ||
writer.call { | ||
generateValue( | ||
writer, value, if (idx < applicableParams.count() - 1) "," else "", false | ||
) | ||
} | ||
} | ||
} | ||
writer.write("let resolver = try \$N()", EndpointTypes.DefaultEndpointResolver).write("") | ||
|
||
testCase.expect.error.ifPresent { error -> | ||
writer.openBlock( | ||
"XCTAssertThrowsError(try resolver.resolve(params: endpointParams)) { error in", "}" | ||
) { | ||
writer.openBlock("switch error {", "}") { | ||
writer.dedent().write("case \$N.unresolved(let message):", ClientRuntimeTypes.Core.EndpointError) | ||
writer.indent().write("XCTAssertEqual(\$S, message)", error) | ||
writer.dedent().write("default:") | ||
writer.indent().write("XCTFail()") | ||
} | ||
} | ||
} | ||
testCase.expect.endpoint.ifPresent { endpoint -> | ||
writer.write("let actual = try resolver.resolve(params: endpointParams)").write("") | ||
|
||
// [String: AnyHashable] can't be constructed from a dictionary literal | ||
// first create a string JSON string literal | ||
// then convert to [String: AnyHashable] using JSONSerialization.jsonObject(with:) | ||
writer.openBlock("let properties: [String: AnyHashable] = ", "") { | ||
generateProperties(writer, endpoint.properties) | ||
} | ||
|
||
val reference = if (endpoint.headers.isNotEmpty()) "var" else "let" | ||
writer.write("$reference headers = \$N()", SmithyHTTPAPITypes.Headers) | ||
endpoint.headers.forEach { (name, values) -> | ||
writer.write("headers.add(name: \$S, values: [\$S])", name, values.sorted().joinToString(",")) | ||
} | ||
writer.write( | ||
"let expected = try \$N(urlString: \$S, headers: headers, properties: properties)", | ||
SmithyHTTPAPITypes.Endpoint, | ||
endpoint.url | ||
).write("") | ||
writer.write("XCTAssertEqual(expected, actual)") | ||
} | ||
} | ||
writer.write("") | ||
} | ||
} | ||
|
||
return count | ||
} | ||
|
||
/** | ||
* Recursively traverse map of properties and generate JSON string literal. | ||
*/ | ||
private fun generateProperties(writer: SwiftWriter, properties: Map<String, Node>) { | ||
if (properties.isEmpty()) { | ||
writer.write("[:]") | ||
} else { | ||
writer.openBlock("[", "]") { | ||
properties.map { it.key to it.value }.forEachIndexed { idx, (first, second) -> | ||
val value = Value.fromNode(second) | ||
writer.writeInline("\$S: ", first) | ||
writer.call { | ||
generateValue(writer, value, if (idx < properties.values.count() - 1) "," else "", true) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Recursively traverse the value and render a JSON string literal. | ||
*/ | ||
private fun generateValue(writer: SwiftWriter, value: Value, delimeter: String, castToAnyHashable: Boolean) { | ||
when (value) { | ||
is StringValue -> { | ||
writer.write("\$S$delimeter", value.toString()) | ||
} | ||
|
||
is IntegerValue -> { | ||
writer.write("\$L$delimeter", value.toString()) | ||
} | ||
|
||
is BooleanValue -> { | ||
writer.write("\$L$delimeter", value.toString()) | ||
} | ||
|
||
is EmptyValue -> { | ||
writer.write("nil$delimeter") | ||
} | ||
|
||
is ArrayValue -> { | ||
val castStmt = if (castToAnyHashable) " as [AnyHashable]$delimeter" else delimeter | ||
writer.openBlock("[", "]$castStmt") { | ||
value.values.forEachIndexed { idx, item -> | ||
writer.call { | ||
generateValue(writer, item, if (idx < value.values.count() - 1) "," else "", castToAnyHashable) | ||
} | ||
} | ||
} | ||
} | ||
|
||
is RecordValue -> { | ||
if (value.value.isEmpty()) { | ||
writer.writeInline("[:]") | ||
} else { | ||
writer.openBlock("[", "] as [String: AnyHashable]$delimeter") { | ||
value.value.map { it.key to it.value }.forEachIndexed { idx, (first, second) -> | ||
writer.writeInline("\$S: ", first.name) | ||
writer.call { | ||
generateValue(writer, second, if (idx < value.value.count() - 1) "," else "", castToAnyHashable) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
else -> { | ||
throw CodegenException("Unsupported value type: $value") | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package software.amazon.smithy.swift.codegen.integration | ||
|
||
import software.amazon.smithy.swift.codegen.SwiftWriter | ||
import software.amazon.smithy.swift.codegen.middleware.OperationMiddleware | ||
|
||
class SmithyHttpProtocolClientGeneratorFactory : HttpProtocolClientGeneratorFactory { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created to support protocol generators in smithy swift |
||
override fun createHttpProtocolClientGenerator( | ||
ctx: ProtocolGenerator.GenerationContext, | ||
httpBindingResolver: HttpBindingResolver, | ||
writer: SwiftWriter, | ||
serviceName: String, | ||
defaultContentType: String, | ||
httpProtocolCustomizable: HTTPProtocolCustomizable, | ||
operationMiddleware: OperationMiddleware | ||
): HttpProtocolClientGenerator { | ||
val config = SmithyServiceConfig(writer, ctx) | ||
return HttpProtocolClientGenerator(ctx, writer, config, httpBindingResolver, defaultContentType, httpProtocolCustomizable, operationMiddleware) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously only AWSClientConfigDefaultsProvider existed. This separates out code that was not AWS specific to allow for supporting protocol generation in the future.