Skip to content
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

Merged
merged 38 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b236c1a
add CBOR support to ServiceUtils (WireProtocol, AWSProtocol)
dayaffe Nov 8, 2024
c6a872c
add configurable protocol resolution with a default priority list
dayaffe Nov 8, 2024
37fdc0e
make function makeQueryCompatibleError generic to work across multipl…
dayaffe Nov 13, 2024
cdbaadb
Merge branch 'main' into day/rpcv2-cbor
dayaffe Dec 2, 2024
10dccdd
current version only has 3/69 failing protocol tests
dayaffe Dec 19, 2024
50ceb88
ALL TESTS PASSING WOOOO
dayaffe Dec 20, 2024
4eec99b
Merge branch 'main' into day/rpcv2-cbor
dayaffe Dec 24, 2024
ebde137
all tests passing now
dayaffe Dec 24, 2024
6b1e91c
add getter for private context
dayaffe Dec 24, 2024
e587f61
Merge branch 'main' into day/rpcv2-cbor
dayaffe Dec 24, 2024
7a321a0
add addUserAgentMiddleware fun to mocks
dayaffe Dec 24, 2024
3e1f884
expose only service name instead of whole context
dayaffe Dec 24, 2024
079a6c3
implement addUserAgentMiddleware in CBOR mock
dayaffe Dec 24, 2024
b8475ce
fix some lint issues
dayaffe Dec 26, 2024
36628b4
fix some lint issues
dayaffe Dec 26, 2024
9b2880b
fix imports
dayaffe Dec 26, 2024
f14c31b
more import fixing
dayaffe Dec 26, 2024
2601702
more lint fixes
dayaffe Dec 26, 2024
2713fbc
more lint
dayaffe Dec 26, 2024
2de2171
try reorder
dayaffe Dec 26, 2024
2b3120e
ran ktlintFormat
dayaffe Dec 26, 2024
5bc3d23
Merge branch 'main' into day/rpcv2-cbor
dayaffe Dec 26, 2024
d7da330
address PR comments
dayaffe Dec 31, 2024
6a607ae
ignore timestampFormat on member for generating reading/writing closures
dayaffe Dec 31, 2024
dde4eba
change format type
dayaffe Dec 31, 2024
40f271f
change format type
dayaffe Dec 31, 2024
3accc45
short circuit timestamp format resolution for cbor
dayaffe Dec 31, 2024
0b9d049
Move CBOR implementation to smithy-swift
dayaffe Jan 29, 2025
cb26666
Merge branch 'main' into day/move-rpcv2
dayaffe Jan 29, 2025
2a1ba81
some changes for lint
dayaffe Jan 29, 2025
da49a1f
more lint fixes
dayaffe Jan 29, 2025
9d64040
create default endpoint middleware
dayaffe Jan 30, 2025
0a795b3
Fix endpoint resolution and protocol generator selection
dayaffe Jan 30, 2025
8dd5737
try to ktlint
dayaffe Jan 30, 2025
791cf36
try to ktlint
dayaffe Jan 30, 2025
7c90fc9
some comment changes
dayaffe Jan 31, 2025
65898df
refactor to create SmithyHTTPBindingProtocolGenerator
dayaffe Feb 3, 2025
b29b7f8
ktlint
dayaffe Feb 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Sources/ClientRuntime/Config/ClientConfigDefaultsProvider.swift
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 {
Copy link
Contributor Author

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.

/// 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
)
}
}
16 changes: 7 additions & 9 deletions Sources/ClientRuntime/Endpoints/EndpointResolverMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}
}
Expand All @@ -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?
Expand Down
50 changes: 50 additions & 0 deletions Sources/ClientRuntime/protocols/rpcv2cbor/RpcV2CborError.swift
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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
}
}
Loading
Loading