diff --git a/Package.swift b/Package.swift index 01783ca80..2aeb077b2 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,7 @@ let package = Package( .library(name: "SmithyTestUtil", targets: ["SmithyTestUtil"]), ], dependencies: [ - .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.20.0"), + .package(url: "https://github.com/awslabs/aws-crt-swift.git", exact: "0.22.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") ], diff --git a/Package.version b/Package.version index 0f1a7dfc7..4ef2eb086 100644 --- a/Package.version +++ b/Package.version @@ -1 +1 @@ -0.37.0 +0.39.0 diff --git a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift index ef4497e05..7d9da83fa 100644 --- a/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift +++ b/Sources/ClientRuntime/Config/DefaultSDKRuntimeConfiguration.swift @@ -90,7 +90,7 @@ public extension DefaultSDKRuntimeConfiguration { /// - Parameter httpClientConfiguration: The configuration for the HTTP client. /// - Returns: The `CRTClientEngine` client on Mac & Linux platforms, returns `URLSessionHttpClient` on non-Mac Apple platforms. static func makeClient(httpClientConfiguration: HttpClientConfiguration) -> HTTPClient { - #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) || os(macOS) return URLSessionHTTPClient(httpClientConfiguration: httpClientConfiguration) #else let connectTimeoutMs = httpClientConfiguration.connectTimeout.map { UInt32($0 * 1_000_000) } diff --git a/Sources/ClientRuntime/Networking/Endpoint.swift b/Sources/ClientRuntime/Networking/Endpoint.swift index 9dbb65857..2671d96ef 100644 --- a/Sources/ClientRuntime/Networking/Endpoint.swift +++ b/Sources/ClientRuntime/Networking/Endpoint.swift @@ -7,7 +7,7 @@ import Foundation public struct Endpoint: Hashable { public let path: String - public let queryItems: [URLQueryItem]? + public let queryItems: [SDKURLQueryItem]? public let protocolType: ProtocolType? public let host: String public let port: Int16 @@ -43,7 +43,7 @@ public struct Endpoint: Hashable { public init(host: String, path: String = "/", port: Int16 = 443, - queryItems: [URLQueryItem]? = nil, + queryItems: [SDKURLQueryItem]? = nil, protocolType: ProtocolType? = .https, headers: Headers? = nil, properties: [String: AnyHashable] = [:]) { diff --git a/Sources/ClientRuntime/Networking/HashFunction.swift b/Sources/ClientRuntime/Networking/HashFunction.swift new file mode 100644 index 000000000..73129da48 --- /dev/null +++ b/Sources/ClientRuntime/Networking/HashFunction.swift @@ -0,0 +1,83 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import AwsCommonRuntimeKit + +enum HashResult { + case data(Data) + case integer(UInt32) +} + +enum HashError: Error { + case invalidInput + case hashingFailed(reason: String) +} + +enum HashFunction { + case crc32, crc32c, sha1, sha256, md5 + + static func from(string: String) -> HashFunction? { + switch string.lowercased() { + case "crc32": return .crc32 + case "crc32c": return .crc32c + case "sha1": return .sha1 + case "sha256": return .sha256 + case "md5": return .md5 // md5 is not a valid flexible checksum algorithm + default: return nil + } + } + + var isSupported: Bool { + switch self { + case .crc32, .crc32c, .sha256, .sha1: + return true + default: + return false + } + } + + func computeHash(of data: Data) throws -> HashResult { + switch self { + case .crc32: + return .integer(data.computeCRC32()) + case .crc32c: + return .integer(data.computeCRC32C()) + case .sha1: + do { + let hashed = try data.computeSHA1() + return .data(hashed) + } catch { + throw HashError.hashingFailed(reason: "Error computing SHA1: \(error)") + } + case .sha256: + do { + let hashed = try data.computeSHA256() + return .data(hashed) + } catch { + throw HashError.hashingFailed(reason: "Error computing SHA256: \(error)") + } + case .md5: + do { + let hashed = try data.computeMD5() + return .data(hashed) + } catch { + throw HashError.hashingFailed(reason: "Error computing MD5: \(error)") + } + } + } +} + +extension HashResult { + + // Convert a HashResult to a hexadecimal String + func toHexString() -> String { + switch self { + case .data(let data): + return data.map { String(format: "%02x", $0) }.joined() + case .integer(let integer): + return String(format: "%08x", integer) + } + } +} diff --git a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift index 5d1bd82bf..42cafcb41 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/CRTClientEngine.swift @@ -85,7 +85,7 @@ public class CRTClientEngine: HTTPClient { clientBootstrap: sharedDefaultIO.clientBootstrap, hostName: endpoint.host, initialWindowSize: windowSize, - port: UInt16(endpoint.port), + port: UInt32(endpoint.port), proxyOptions: nil, socketOptions: socketOptions, tlsOptions: tlsConnectionOptions, @@ -118,7 +118,7 @@ public class CRTClientEngine: HTTPClient { let options = HTTP2StreamManagerOptions( clientBootstrap: sharedDefaultIO.clientBootstrap, hostName: endpoint.host, - port: UInt16(endpoint.port), + port: UInt32(endpoint.port), maxConnections: maxConnectionsPerEndpoint, socketOptions: socketOptions, tlsOptions: tlsConnectionOptions, diff --git a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift index 383aa7260..bbb29be5b 100644 --- a/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift +++ b/Sources/ClientRuntime/Networking/Http/CRT/HTTP2Stream+ByteStream.swift @@ -21,14 +21,14 @@ extension HTTP2Stream { func write(body: ByteStream) async throws { switch body { case .data(let data): - try await writeData(data: data ?? .init(), endOfStream: true) + try await writeChunk(chunk: data ?? .init(), endOfStream: true) case .stream(let stream): while let data = try await stream.readAsync(upToCount: manualWriteBufferSize) { - try await writeData(data: data, endOfStream: false) + try await writeChunk(chunk: data, endOfStream: false) } - try await writeData(data: .init(), endOfStream: true) + try await writeChunk(chunk: .init(), endOfStream: true) case .noStream: - try await writeData(data: .init(), endOfStream: true) + try await writeChunk(chunk: .init(), endOfStream: true) } } } diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift index ec61bd458..1cbaccd11 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift @@ -20,13 +20,11 @@ public struct ContentMD5Middleware: Middleware { switch input.body { case .data(let data): - guard - let data = data, - let bodyString = String(data: data, encoding: .utf8) - else { + guard let data = data else { return try await next.handle(context: context, input: input) } - let base64Encoded = try bodyString.base64EncodedMD5() + let md5Hash = try data.computeMD5() + let base64Encoded = md5Hash.base64EncodedString() input.headers.update(name: "Content-MD5", value: base64Encoded) case .stream: guard let logger = context.getLogger() else { diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/HeaderMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/HeaderMiddleware.swift index db2393a41..29d833521 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/HeaderMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/HeaderMiddleware.swift @@ -5,10 +5,14 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct HeaderMiddleware: Middleware { +public struct HeaderMiddleware: Middleware { public let id: String = "\(String(describing: OperationStackInput.self))HeadersMiddleware" - public init() {} + let headerProvider: HeaderProvider + + public init(_ headerProvider: @escaping HeaderProvider) { + self.headerProvider = headerProvider + } public func handle(context: Context, input: MInput, @@ -17,8 +21,7 @@ public struct HeaderMiddleware = (T) -> Headers diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/Providers/QueryItemProvider.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/Providers/QueryItemProvider.swift index a9fcad7f1..97e92a2e0 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/Providers/QueryItemProvider.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/Providers/QueryItemProvider.swift @@ -5,6 +5,4 @@ // SPDX-License-Identifier: Apache-2.0 // -public protocol QueryItemProvider { - var queryItems: [URLQueryItem] { get throws } -} +public typealias QueryItemProvider = (T) throws -> [SDKURLQueryItem] diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/Providers/URLPathProvider.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/Providers/URLPathProvider.swift index 7c30883fd..63b76f61c 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/Providers/URLPathProvider.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/Providers/URLPathProvider.swift @@ -5,6 +5,4 @@ // SPDX-License-Identifier: Apache-2.0 // -public protocol URLPathProvider { - var urlPath: String? { get } -} +public typealias URLPathProvider = (T) -> String? diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/QueryItemMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/QueryItemMiddleware.swift index a8370152e..2e0f2235f 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/QueryItemMiddleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/QueryItemMiddleware.swift @@ -5,10 +5,14 @@ // SPDX-License-Identifier: Apache-2.0 // -public struct QueryItemMiddleware: Middleware { +public struct QueryItemMiddleware: Middleware { public let id: String = "\(String(describing: OperationStackInput.self))QueryItemMiddleware" - public init() {} + let queryItemProvider: QueryItemProvider + + public init(_ queryItemProvider: @escaping QueryItemProvider) { + self.queryItemProvider = queryItemProvider + } public func handle(context: Context, input: MInput, @@ -17,7 +21,7 @@ public struct QueryItemMiddleware: Middleware { +public struct URLPathMiddleware: Middleware { public let id: Swift.String = "\(String(describing: OperationStackInput.self))URLPathMiddleware" let urlPrefix: Swift.String? + let urlPathProvider: URLPathProvider - public init(urlPrefix: Swift.String? = nil) { + public init(urlPrefix: Swift.String? = nil, _ urlPathProvider: @escaping URLPathProvider) { self.urlPrefix = urlPrefix + self.urlPathProvider = urlPathProvider } public func handle(context: Context, @@ -21,7 +23,7 @@ public struct URLPathMiddleware SdkHttpRequestBuilder { + public func withQueryItems(_ value: [SDKURLQueryItem]) -> SdkHttpRequestBuilder { self.queryItems = self.queryItems ?? [] self.queryItems?.append(contentsOf: value) return self } @discardableResult - public func withQueryItem(_ value: URLQueryItem) -> SdkHttpRequestBuilder { + public func withQueryItem(_ value: SDKURLQueryItem) -> SdkHttpRequestBuilder { withQueryItems([value]) } diff --git a/Sources/ClientRuntime/PrimitiveTypeExtensions/URL+Extension.swift b/Sources/ClientRuntime/PrimitiveTypeExtensions/URL+Extension.swift index 1e12f159d..791ff1fe1 100644 --- a/Sources/ClientRuntime/PrimitiveTypeExtensions/URL+Extension.swift +++ b/Sources/ClientRuntime/PrimitiveTypeExtensions/URL+Extension.swift @@ -9,9 +9,9 @@ public typealias URL = Foundation.URL extension URL { - func toQueryItems() -> [URLQueryItem]? { + func toQueryItems() -> [SDKURLQueryItem]? { URLComponents(url: self, resolvingAgainstBaseURL: false)? .queryItems? - .map { URLQueryItem(name: $0.name, value: $0.value) } + .map { SDKURLQueryItem(name: $0.name, value: $0.value) } } } diff --git a/Sources/ClientRuntime/PrimitiveTypeExtensions/URLQueryItem+Extensions.swift b/Sources/ClientRuntime/PrimitiveTypeExtensions/URLQueryItem+Extensions.swift index 67118e619..d81ee9a20 100644 --- a/Sources/ClientRuntime/PrimitiveTypeExtensions/URLQueryItem+Extensions.swift +++ b/Sources/ClientRuntime/PrimitiveTypeExtensions/URLQueryItem+Extensions.swift @@ -3,9 +3,7 @@ * SPDX-License-Identifier: Apache-2.0. */ -public typealias URLQueryItem = MyURLQueryItem - -public struct MyURLQueryItem: Hashable { +public struct SDKURLQueryItem: Hashable { public var name: String public var value: String? diff --git a/Sources/ClientRuntime/Retries/DefaultRetryErrorInfoProvider.swift b/Sources/ClientRuntime/Retries/DefaultRetryErrorInfoProvider.swift index c77f2c7c3..60d0df2ec 100644 --- a/Sources/ClientRuntime/Retries/DefaultRetryErrorInfoProvider.swift +++ b/Sources/ClientRuntime/Retries/DefaultRetryErrorInfoProvider.swift @@ -25,17 +25,12 @@ public enum DefaultRetryErrorInfoProvider: RetryErrorInfoProvider { hint = TimeInterval(retryAfterString) } if let modeledError = error as? ModeledError { - switch type(of: modeledError).isThrottling { - case true: - return .init(errorType: .throttling, retryAfterHint: hint, isTimeout: false) - case false: - let errorType = type(of: modeledError).fault.retryErrorType - return .init(errorType: errorType, retryAfterHint: hint, isTimeout: false) - } - } else if let httpError = error as? HTTPError { - if retryableStatusCodes.contains(httpError.httpResponse.statusCode) { - return .init(errorType: .serverError, retryAfterHint: hint, isTimeout: false) - } + let type = type(of: modeledError) + guard type.isRetryable else { return nil } + let errorType: RetryErrorType = type.isThrottling ? .throttling : type.fault.retryErrorType + return .init(errorType: errorType, retryAfterHint: hint, isTimeout: false) + } else if let code = (error as? HTTPError)?.httpResponse.statusCode, retryableStatusCodes.contains(code) { + return .init(errorType: .serverError, retryAfterHint: hint, isTimeout: false) } return nil } diff --git a/Sources/SmithyTestUtil/RequestTestUtil/ExpectedSdkHttpRequest.swift b/Sources/SmithyTestUtil/RequestTestUtil/ExpectedSdkHttpRequest.swift index 0d20e8fae..d8ba0506b 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/ExpectedSdkHttpRequest.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/ExpectedSdkHttpRequest.swift @@ -12,9 +12,9 @@ public struct ExpectedSdkHttpRequest { public var headers: Headers? public var forbiddenHeaders: [String]? public var requiredHeaders: [String]? - public let queryItems: [URLQueryItem]? - public let forbiddenQueryItems: [URLQueryItem]? - public let requiredQueryItems: [URLQueryItem]? + public let queryItems: [SDKURLQueryItem]? + public let forbiddenQueryItems: [SDKURLQueryItem]? + public let requiredQueryItems: [SDKURLQueryItem]? public let endpoint: Endpoint public let method: HttpMethodType @@ -23,9 +23,9 @@ public struct ExpectedSdkHttpRequest { headers: Headers? = nil, forbiddenHeaders: [String]? = nil, requiredHeaders: [String]? = nil, - queryItems: [URLQueryItem]? = nil, - forbiddenQueryItems: [URLQueryItem]? = nil, - requiredQueryItems: [URLQueryItem]? = nil, + queryItems: [SDKURLQueryItem]? = nil, + forbiddenQueryItems: [SDKURLQueryItem]? = nil, + requiredQueryItems: [SDKURLQueryItem]? = nil, body: ByteStream = ByteStream.noStream) { self.method = method self.endpoint = endpoint @@ -50,9 +50,9 @@ public class ExpectedSdkHttpRequestBuilder { var host: String = "" var path: String = "/" var body: ByteStream = .noStream - var queryItems = [URLQueryItem]() - var forbiddenQueryItems = [URLQueryItem]() - var requiredQueryItems = [URLQueryItem]() + var queryItems = [SDKURLQueryItem]() + var forbiddenQueryItems = [SDKURLQueryItem]() + var requiredQueryItems = [SDKURLQueryItem]() var port: Int16 = 443 var protocolType: ProtocolType = .https @@ -109,19 +109,19 @@ public class ExpectedSdkHttpRequestBuilder { } @discardableResult - public func withQueryItem(_ value: URLQueryItem) -> ExpectedSdkHttpRequestBuilder { + public func withQueryItem(_ value: SDKURLQueryItem) -> ExpectedSdkHttpRequestBuilder { self.queryItems.append(value) return self } @discardableResult - public func withForbiddenQueryItem(_ value: URLQueryItem) -> ExpectedSdkHttpRequestBuilder { + public func withForbiddenQueryItem(_ value: SDKURLQueryItem) -> ExpectedSdkHttpRequestBuilder { self.forbiddenQueryItems.append(value) return self } @discardableResult - public func withRequiredQueryItem(_ value: URLQueryItem) -> ExpectedSdkHttpRequestBuilder { + public func withRequiredQueryItem(_ value: SDKURLQueryItem) -> ExpectedSdkHttpRequestBuilder { self.requiredQueryItems.append(value) return self } diff --git a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift index 47c090621..dbac335af 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase+FormURL.swift @@ -20,12 +20,12 @@ extension HttpRequestTestBase { assertQueryItems(expectedQueryItems, actualQueryItems, file: file, line: line) } - private func convertToQueryItems(data: Data) -> [ClientRuntime.URLQueryItem] { + private func convertToQueryItems(data: Data) -> [SDKURLQueryItem] { guard let queryString = String(data: data, encoding: .utf8) else { XCTFail("Failed to decode data") return [] } - var queryItems: [ClientRuntime.URLQueryItem] = [] + var queryItems: [SDKURLQueryItem] = [] let sanitizedQueryString = queryString.replacingOccurrences(of: "\n", with: "") let keyValuePairs = sanitizedQueryString.components(separatedBy: "&") for keyValue in keyValuePairs { @@ -36,7 +36,7 @@ extension HttpRequestTestBase { } let name: String = keyValueArray[0] let value = keyValueArray.count >= 2 ? sanitizeStringForNonConformingValues(keyValueArray[1]) : nil - queryItems.append(URLQueryItem(name: name, value: value)) + queryItems.append(SDKURLQueryItem(name: name, value: value)) } return queryItems } diff --git a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift index b96d2da25..e75c9d60e 100644 --- a/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift +++ b/Sources/SmithyTestUtil/RequestTestUtil/HttpRequestTestBase.swift @@ -116,10 +116,10 @@ open class HttpRequestTestBase: XCTestCase { if queryParamComponents.count > 1 { let value = sanitizeStringForNonConformingValues(queryParamComponents[1]) - builder.withQueryItem(URLQueryItem(name: queryParamComponents[0], + builder.withQueryItem(SDKURLQueryItem(name: queryParamComponents[0], value: value)) } else { - builder.withQueryItem(URLQueryItem(name: queryParamComponents[0], value: nil)) + builder.withQueryItem(SDKURLQueryItem(name: queryParamComponents[0], value: nil)) } } } @@ -130,10 +130,10 @@ open class HttpRequestTestBase: XCTestCase { if queryParamComponents.count > 1 { let value = sanitizeStringForNonConformingValues(queryParamComponents[1]) - builder.withForbiddenQueryItem(URLQueryItem(name: queryParamComponents[0], + builder.withForbiddenQueryItem(SDKURLQueryItem(name: queryParamComponents[0], value: value)) } else { - builder.withForbiddenQueryItem(URLQueryItem(name: queryParamComponents[0], value: nil)) + builder.withForbiddenQueryItem(SDKURLQueryItem(name: queryParamComponents[0], value: nil)) } } } @@ -144,10 +144,10 @@ open class HttpRequestTestBase: XCTestCase { if queryParamComponents.count > 1 { let value = sanitizeStringForNonConformingValues(queryParamComponents[1]) - builder.withRequiredQueryItem(URLQueryItem(name: queryParamComponents[0], + builder.withRequiredQueryItem(SDKURLQueryItem(name: queryParamComponents[0], value: value)) } else { - builder.withRequiredQueryItem(URLQueryItem(name: queryParamComponents[0], value: nil)) + builder.withRequiredQueryItem(SDKURLQueryItem(name: queryParamComponents[0], value: nil)) } } } @@ -165,7 +165,7 @@ open class HttpRequestTestBase: XCTestCase { /** Check if a Query Item with given name exists in array of `URLQueryItem` */ - public func queryItemExists(_ queryItemName: String, in queryItems: [ClientRuntime.URLQueryItem]?) -> Bool { + public func queryItemExists(_ queryItemName: String, in queryItems: [SDKURLQueryItem]?) -> Bool { guard let queryItems = queryItems else { return false } @@ -347,8 +347,8 @@ open class HttpRequestTestBase: XCTestCase { } public func assertQueryItems( - _ expected: [ClientRuntime.URLQueryItem]?, - _ actual: [ClientRuntime.URLQueryItem]?, + _ expected: [SDKURLQueryItem]?, + _ actual: [SDKURLQueryItem]?, file: StaticString = #filePath, line: UInt = #line ) { @@ -381,8 +381,8 @@ open class HttpRequestTestBase: XCTestCase { } public func assertForbiddenQueryItems( - _ expected: [ClientRuntime.URLQueryItem]?, - _ actual: [ClientRuntime.URLQueryItem]?, + _ expected: [SDKURLQueryItem]?, + _ actual: [SDKURLQueryItem]?, file: StaticString = #filePath, line: UInt = #line ) { @@ -404,8 +404,8 @@ open class HttpRequestTestBase: XCTestCase { } public func assertRequiredQueryItems( - _ expected: [ClientRuntime.URLQueryItem]?, - _ actual: [ClientRuntime.URLQueryItem]?, + _ expected: [SDKURLQueryItem]?, + _ actual: [SDKURLQueryItem]?, file: StaticString = #filePath, line: UInt = #line ) { diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift index 1c964de81..302a590d6 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/MiddlewareTests/ProviderTests.swift @@ -15,7 +15,7 @@ class ProviderTests: HttpRequestTestBase { var mockInput = MockInput() mockInput.value = 3 - XCTAssert(mockInput.urlPath == "/3") + XCTAssert(MockInput.urlPathProvider(mockInput) == "/3") } func testURLPathMiddleware() async throws { @@ -25,7 +25,7 @@ class ProviderTests: HttpRequestTestBase { let context = HttpContextBuilder().withDecoder(value: JSONDecoder()).build() var operationStack = OperationStack(id: "testURLPathOperation") - operationStack.initializeStep.intercept(position: .after, middleware: URLPathMiddleware()) + operationStack.initializeStep.intercept(position: .after, middleware: URLPathMiddleware(MockInput.urlPathProvider(_:))) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware(id: "TestDeserializeMiddleware")) _ = try await operationStack.handleMiddleware(context: context, input: mockInput, @@ -45,7 +45,7 @@ class ProviderTests: HttpRequestTestBase { let context = HttpContextBuilder().withDecoder(value: JSONDecoder()).build() var operationStack = OperationStack(id: "testURLPathOperation") - operationStack.serializeStep.intercept(position: .after, middleware: QueryItemMiddleware()) + operationStack.serializeStep.intercept(position: .after, middleware: QueryItemMiddleware(MockInput.queryItemProvider(_:))) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware(id: "TestDeserializeMiddleware")) _ = try await operationStack.handleMiddleware(context: context, input: mockInput, @@ -65,7 +65,7 @@ class ProviderTests: HttpRequestTestBase { var mockInput = MockInput() mockInput.value = 3 - XCTAssert(mockInput.headers.headers.count == 1) + XCTAssert(MockInput.headerProvider(mockInput).headers.count == 1) } func testHeaderMiddleware() async throws { @@ -75,7 +75,7 @@ class ProviderTests: HttpRequestTestBase { let context = HttpContextBuilder().withDecoder(value: JSONDecoder()).build() var operationStack = OperationStack(id: "testURLPathOperation") - operationStack.serializeStep.intercept(position: .after, middleware: HeaderMiddleware()) + operationStack.serializeStep.intercept(position: .after, middleware: HeaderMiddleware(MockInput.headerProvider(_:))) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware(id: "TestDeserializeMiddleware")) _ = try await operationStack.handleMiddleware(context: context, input: mockInput, @@ -92,28 +92,29 @@ class ProviderTests: HttpRequestTestBase { } } -extension MockInput: URLPathProvider, QueryItemProvider, HeaderProvider { - public var urlPath: String? { - guard let value = value else { +extension MockInput { + + static func urlPathProvider(_ mock: MockInput) -> String? { + guard let value = mock.value else { return nil } return "/\(value)" } - public var queryItems: [ClientRuntime.URLQueryItem] { - var items = [ClientRuntime.URLQueryItem]() + static func queryItemProvider(_ mock: MockInput) -> [SDKURLQueryItem] { + var items = [SDKURLQueryItem]() - if let value = value { - let valueQueryItem = ClientRuntime.URLQueryItem(name: "test", value: "\(value)") + if let value = mock.value { + let valueQueryItem = SDKURLQueryItem(name: "test", value: "\(value)") items.append(valueQueryItem) } return items } - public var headers: Headers { + static func headerProvider(_ mock: MockInput) -> Headers { var items = Headers() - if let value = value { + if let value = mock.value { let headerItem = Header(name: "test", value: "\(value)") items.add(headerItem) } diff --git a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/SdkRequestBuilderTests.swift b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/SdkRequestBuilderTests.swift index 5c62d6e0b..d6b18d1c3 100644 --- a/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/SdkRequestBuilderTests.swift +++ b/Tests/ClientRuntimeTests/ClientRuntimeTests/NetworkingTests/SdkRequestBuilderTests.swift @@ -11,7 +11,7 @@ class SdkRequestBuilderTests: XCTestCase { func testSdkRequestBuilderUpdate() throws { let url = "https://stehlibstoragebucket144955-dev.s3.us-east-1.amazonaws.com/public/README.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAVIBY7SZR4C4MBGAW%2F20220225%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220225T034419Z&X-Amz-Expires=17999&X-Amz-SignedHeaders=host&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEQaCXVzLWVhc3QtMSJHMEUCIEKUDdnCf1h3cZNdv10q6G24zLgn0r6th4m%2FXSS9TuR4AiEAwOwf2cG%2F1W%2FkAz1UMqFW9sZp7SY6j%2BmiicLy1dB8OXUqmgYInf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgwzNjA4OTY3NjM0OTEiDH6CiNUJiwL4sNy9KCruBbdJnGSx88A%2Be0SBpKEpSurxunasaDsJb2ZJPqVhC%2FcKPr87PYcp5BrnkGumcYhUAEknVbHACLLUx2%2Fnzfacp13PHmclSsLe52qGwjFFVMz0m41PV3HiCoHgIc7vVIHnwNySaX9ZJbVowhNvI4V9eixVKhjjx7Tqku1bsOzlq9dJP15qz8FVNjlKGjZFqrMGTzpTgmS9dNPqphwCcxx306RLUd35SEvXjxnWcidOdddYQs9j4wu47DOM8ftveag6cDptJQN71dDIRHgFTisVshD78Rm9pycKf3g0QvBAGtrzhcxUcJtNgIWv%2B10hsEBURsEYommcjI8vT1yX2K8pLVOxgL%2FRWXndbAeIzAu5vmLm6RqkfGwkHQPQl7uII6YzL2Gku%2FMDilVFw9TBKIg6KDP9l2GzzVRQsvLMpFIp%2Bx3a1s4OVduJRFpDYCwEsfKhIoVkb610gBbFayPKjQVcZfULdq1r5DOZzpHVDoijnKHAxHtFgaFPP6KtG%2BmdKeix8gccdbsdgMokWKtJhisFo%2BzLn02oSSX4ITkZKzZcriGxQO4E1YUlYyBhjlCg1b74faQfWstk24PrkCfNXYcQ5oxgglIA0tBOdOfwGn2Je3MBEj2T8Yz0GS%2BZib3DKVWRzU0Xk9pwDXH3iaBn9Uld%2BNyw9gxdOCBVKtTILtdfsjw9lJSVOJomlJn1h8gH96PToBg9lOc4ms6aA2Z%2FoN%2F3UV%2Beo5%2FpB9xfMkBIeOg6vAI0VtjkUhH052UouEKU%2BVGSGuCSuVzZmIBJLWHZaQUJZ3hJCdGqM%2FoM4Ud51Cidcnr%2Bni%2Bgp4RAfg0gvX6Eb2e%2FNMqNd0Eg7ftmnPXzQR3ZVC1yyNFu2kBt8icKdMnkLZT8YO1Racd1QgrZ5DZJlU%2FS2eisLGSTMb9Dmaq8AGbxgGK55acIvsQLzvJhavFWbh%2FyNudQBSLC0c5BZGxQk2J%2BJx%2FlR5wR%2B0gXOfTg5EImMrLzh7gOo56i2Rhj2xRFSDDDnuGQBjqHArU56DsGZNeKwkVK87lVJAfiAsWmKaDlBNXcuhKl084syaZMiVUQa7dPupgeg%2BvVPv4dAwNULjuE3B7V5lSea4RIDOfGwTtlj4Ekn3t4PrlxHpEyMeuRgekNhpdcfL5pgRcKH4AKgqI7fCrghhH1qDMTpUkORRv6EwPGqM0LPm93XPVOjrG3EJ5ZOYLJM05bbrcalzN0mbSbsRrcphme7Z8zpD5jNeSWdBhWA04DQ7RZuJpqFnn40yKq7G8kgtLmOJGRs7CGTWT9on7EOpH3RdKGPXL06%2BgLo7r9%2Fdkb%2BHGJF6spjqbH0SN%2FySjnvgNL1NrGAy%2FQgPwfDw6oJ6TPNdH8dfM8tmdd&X-Amz-Signature=90b9f3353ad4f2596fdd166c00da8fb86d5d255bf3c8b296c5e5a7db25fd41aa" let pathToMatch = "/public/README.md?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAVIBY7SZR4C4MBGAW%2F20220225%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220225T034419Z&X-Amz-Expires=17999&X-Amz-SignedHeaders=host&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEQaCXVzLWVhc3QtMSJHMEUCIEKUDdnCf1h3cZNdv10q6G24zLgn0r6th4m%2FXSS9TuR4AiEAwOwf2cG%2F1W%2FkAz1UMqFW9sZp7SY6j%2BmiicLy1dB8OXUqmgYInf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgwzNjA4OTY3NjM0OTEiDH6CiNUJiwL4sNy9KCruBbdJnGSx88A%2Be0SBpKEpSurxunasaDsJb2ZJPqVhC%2FcKPr87PYcp5BrnkGumcYhUAEknVbHACLLUx2%2Fnzfacp13PHmclSsLe52qGwjFFVMz0m41PV3HiCoHgIc7vVIHnwNySaX9ZJbVowhNvI4V9eixVKhjjx7Tqku1bsOzlq9dJP15qz8FVNjlKGjZFqrMGTzpTgmS9dNPqphwCcxx306RLUd35SEvXjxnWcidOdddYQs9j4wu47DOM8ftveag6cDptJQN71dDIRHgFTisVshD78Rm9pycKf3g0QvBAGtrzhcxUcJtNgIWv%2B10hsEBURsEYommcjI8vT1yX2K8pLVOxgL%2FRWXndbAeIzAu5vmLm6RqkfGwkHQPQl7uII6YzL2Gku%2FMDilVFw9TBKIg6KDP9l2GzzVRQsvLMpFIp%2Bx3a1s4OVduJRFpDYCwEsfKhIoVkb610gBbFayPKjQVcZfULdq1r5DOZzpHVDoijnKHAxHtFgaFPP6KtG%2BmdKeix8gccdbsdgMokWKtJhisFo%2BzLn02oSSX4ITkZKzZcriGxQO4E1YUlYyBhjlCg1b74faQfWstk24PrkCfNXYcQ5oxgglIA0tBOdOfwGn2Je3MBEj2T8Yz0GS%2BZib3DKVWRzU0Xk9pwDXH3iaBn9Uld%2BNyw9gxdOCBVKtTILtdfsjw9lJSVOJomlJn1h8gH96PToBg9lOc4ms6aA2Z%2FoN%2F3UV%2Beo5%2FpB9xfMkBIeOg6vAI0VtjkUhH052UouEKU%2BVGSGuCSuVzZmIBJLWHZaQUJZ3hJCdGqM%2FoM4Ud51Cidcnr%2Bni%2Bgp4RAfg0gvX6Eb2e%2FNMqNd0Eg7ftmnPXzQR3ZVC1yyNFu2kBt8icKdMnkLZT8YO1Racd1QgrZ5DZJlU%2FS2eisLGSTMb9Dmaq8AGbxgGK55acIvsQLzvJhavFWbh%2FyNudQBSLC0c5BZGxQk2J%2BJx%2FlR5wR%2B0gXOfTg5EImMrLzh7gOo56i2Rhj2xRFSDDDnuGQBjqHArU56DsGZNeKwkVK87lVJAfiAsWmKaDlBNXcuhKl084syaZMiVUQa7dPupgeg%2BvVPv4dAwNULjuE3B7V5lSea4RIDOfGwTtlj4Ekn3t4PrlxHpEyMeuRgekNhpdcfL5pgRcKH4AKgqI7fCrghhH1qDMTpUkORRv6EwPGqM0LPm93XPVOjrG3EJ5ZOYLJM05bbrcalzN0mbSbsRrcphme7Z8zpD5jNeSWdBhWA04DQ7RZuJpqFnn40yKq7G8kgtLmOJGRs7CGTWT9on7EOpH3RdKGPXL06%2BgLo7r9%2Fdkb%2BHGJF6spjqbH0SN%2FySjnvgNL1NrGAy%2FQgPwfDw6oJ6TPNdH8dfM8tmdd&X-Amz-Signature=90b9f3353ad4f2596fdd166c00da8fb86d5d255bf3c8b296c5e5a7db25fd41aa" - let queryItems = [ClientRuntime.URLQueryItem(name: "Bucket", value: "stehlibstoragebucket144955-dev"), URLQueryItem(name: "Key", value: "public%2FREADME.md")] + let queryItems = [SDKURLQueryItem(name: "Bucket", value: "stehlibstoragebucket144955-dev"), SDKURLQueryItem(name: "Key", value: "public%2FREADME.md")] let originalRequest = SdkHttpRequest(method: .get, endpoint: Endpoint(host: "stehlibstoragebucket144955-dev.s3.us-east-1.amazonaws.com", path: "/", port: 80, queryItems: queryItems, protocolType: .https)) let crtRequest = try HTTPRequest() crtRequest.path = pathToMatch diff --git a/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift index e3649d763..b86a286a8 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/EndpointTests.swift @@ -15,9 +15,9 @@ class EndpointTests: XCTestCase { func test_queryItems_setsQueryItemsFromURLInOrder() throws { let endpoint = try Endpoint(url: url) let expectedQueryItems = [ - ClientRuntime.URLQueryItem(name: "abc", value: "def"), - URLQueryItem(name: "ghi", value: "jkl"), - URLQueryItem(name: "mno", value: "pqr") + SDKURLQueryItem(name: "abc", value: "def"), + SDKURLQueryItem(name: "ghi", value: "jkl"), + SDKURLQueryItem(name: "mno", value: "pqr") ] XCTAssertEqual(endpoint.queryItems, expectedQueryItems) } diff --git a/Tests/ClientRuntimeTests/NetworkingTests/HashFunctionTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/HashFunctionTests.swift new file mode 100644 index 000000000..d8e394e48 --- /dev/null +++ b/Tests/ClientRuntimeTests/NetworkingTests/HashFunctionTests.swift @@ -0,0 +1,163 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import XCTest +@testable import ClientRuntime +import AwsCommonRuntimeKit + +class HashFunctionTests: XCTestCase { + + override func setUp() { + // Initialize function needs to be called before interacting with CRT + CommonRuntimeKit.initialize() + } + + func testCRC32NonUTF8Bytes() { + guard let hashFunction = HashFunction.from(string: "crc32") else { + XCTFail("CRC32 not found") + return + } + + // Create test data + let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8 + let testData = Data(testBytes) + + let computedHash = try? hashFunction.computeHash(of: testData) + guard case let .integer(result)? = computedHash else { + XCTFail("CRC32 computed hash is not an integer or is nil") + return + } + let expected = UInt32(1426237168) + XCTAssertEqual(result, expected, "CRC32 hash does not match expected value") + } + + func testCRC32CNonUTF8Bytes() { + guard let hashFunction = HashFunction.from(string: "crc32c") else { + XCTFail("CRC32C not found") + return + } + + // Create test data + let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8 + let testData = Data(testBytes) + + let computedHash = try? hashFunction.computeHash(of: testData) + guard case let .integer(result)? = computedHash else { + XCTFail("CRC32C computed hash is not an integer or is nil") + return + } + let expected = UInt32(1856745115) + XCTAssertEqual(result, expected, "CRC32C hash does not match expected value") + } + + func testSHA1NonUTF8Bytes() { + guard let hashFunction = HashFunction.from(string: "sha1") else { + XCTFail("SHA1 not found") + return + } + + // Create test data + let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8 + let testData = Data(testBytes) + + let computedHash = try? hashFunction.computeHash(of: testData) + guard case let .data(result)? = computedHash else { + XCTFail("SHA1 computed hash is not a data type or is nil") + return + } + let expected = "ADfJtWg8Do2MpnFNsvFRmyMuEOI=" + XCTAssertEqual(result.base64EncodedString(), expected, "SHA1 hash does not match expected value") + } + + func testSHA256NonUTF8Bytes() { + guard let hashFunction = HashFunction.from(string: "sha256") else { + XCTFail("SHA256 not found") + return + } + + // Create test data + let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8 + let testData = Data(testBytes) + + let computedHash = try? hashFunction.computeHash(of: testData) + guard case let .data(result)? = computedHash else { + XCTFail("SHA256 computed hash is not a data type or is nil") + return + } + let expected = "jCosV0rEcc6HWQwT8O/bQr0ssZuxhJM3nUW/zJBgtlc=" + XCTAssertEqual(result.base64EncodedString(), expected, "SHA256 hash does not match expected value") + } + + func testMD5NonUTF8Bytes() { + guard let hashFunction = HashFunction.from(string: "md5") else { + XCTFail("MD5 not found") + return + } + + // Create test data + let testBytes: [UInt8] = [0xFF, 0xFE, 0xFD, 0xFC] // Bytes not valid in UTF-8 + let testData = Data(testBytes) + + let computedHash = try? hashFunction.computeHash(of: testData) + guard case let .data(result)? = computedHash else { + XCTFail("MD5 computed hash is not a data type or is nil") + return + } + let expected = "ilWq/WLcPzYHQ8fAzwCCLg==" + XCTAssertEqual(result.base64EncodedString(), expected, "MD5 hash does not match expected value") + } + + func testInvalidHashFunction() { + let invalidHashFunction = HashFunction.from(string: "invalid") + XCTAssertNil(invalidHashFunction, "Invalid hash function should return nil") + } + + func testHashFunctionToHexString() { + let testData = Data("Hello, world!".utf8) + + // CRC32 + if let crc32Function = HashFunction.from(string: "crc32"), + let crc32Result = try? crc32Function.computeHash(of: testData).toHexString() { + XCTAssertEqual(crc32Result, "ebe6c6e6", "CRC32 hexadecimal representation does not match expected value") + } else { + XCTFail("CRC32 hash function not found or computation failed") + } + + // CRC32C + if let crc32cFunction = HashFunction.from(string: "crc32c"), + let crc32cResult = try? crc32cFunction.computeHash(of: testData).toHexString() { + XCTAssertEqual(crc32cResult, "c8a106e5", "CRC32C hexadecimal representation does not match expected value") + } else { + XCTFail("CRC32C hash function not found or computation failed") + } + + // SHA1 + if let sha1Function = HashFunction.from(string: "sha1"), + let sha1Result = try? sha1Function.computeHash(of: testData).toHexString() { + XCTAssertEqual(sha1Result, "943a702d06f34599aee1f8da8ef9f7296031d699", "SHA1 hexadecimal representation does not match expected value") + } else { + XCTFail("SHA1 hash function not found or computation failed") + } + + // SHA256 + if let sha256Function = HashFunction.from(string: "sha256"), + let sha256Result = try? sha256Function.computeHash(of: testData).toHexString() { + XCTAssertEqual(sha256Result, "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", "SHA256 hexadecimal representation does not match expected value") + } else { + XCTFail("SHA256 hash function not found or computation failed") + } + + // MD5 + if let md5Function = HashFunction.from(string: "md5"), + let md5Result = try? md5Function.computeHash(of: testData).toHexString() { + XCTAssertEqual(md5Result, "6cd3556deb0da54bca060b4c39479839", "MD5 hexadecimal representation does not match expected value") + } else { + XCTFail("MD5 hash function not found or computation failed") + } + } + +} diff --git a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift index 798eeb207..1b7d5a433 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/HttpRequestTests.swift @@ -96,8 +96,8 @@ class HttpRequestTests: NetworkingTestUtils { } func testSdkPathAndQueryItemsToCRTPathAndQueryItems() throws { - let queryItem1 = ClientRuntime.URLQueryItem(name: "foo", value: "bar") - let queryItem2 = ClientRuntime.URLQueryItem(name: "quz", value: "baz") + let queryItem1 = SDKURLQueryItem(name: "foo", value: "bar") + let queryItem2 = SDKURLQueryItem(name: "quz", value: "baz") let builder = SdkHttpRequestBuilder() .withHeader(name: "Host", value: "amazon.aws.com") .withPath("/hello") @@ -109,8 +109,8 @@ class HttpRequestTests: NetworkingTestUtils { } func testCRTPathAndQueryItemsToSdkPathAndQueryItems() throws { - let queryItem1 = ClientRuntime.URLQueryItem(name: "foo", value: "bar") - let queryItem2 = ClientRuntime.URLQueryItem(name: "quz", value: "bar") + let queryItem1 = SDKURLQueryItem(name: "foo", value: "bar") + let queryItem2 = SDKURLQueryItem(name: "quz", value: "bar") let builder = SdkHttpRequestBuilder() .withHeader(name: "Host", value: "amazon.aws.com") .withPath("/hello") @@ -128,7 +128,7 @@ class HttpRequestTests: NetworkingTestUtils { XCTAssert(updatedRequest.queryItems?.count == 3) XCTAssert(updatedRequest.queryItems?.contains(queryItem1) ?? false) XCTAssert(updatedRequest.queryItems?.contains(queryItem2) ?? false) - XCTAssert(updatedRequest.queryItems?.contains(ClientRuntime.URLQueryItem(name: "signedthing", value: "signed")) ?? false) + XCTAssert(updatedRequest.queryItems?.contains(SDKURLQueryItem(name: "signedthing", value: "signed")) ?? false) } func testPathInInHttpRequestIsNotAltered() throws { diff --git a/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift b/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift index 68803be8e..30fb4cf16 100644 --- a/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift +++ b/Tests/ClientRuntimeTests/NetworkingTests/NetworkingTestUtils.swift @@ -48,10 +48,10 @@ class NetworkingTestUtils: XCTestCase { func getMockEndpoint(headers: Headers) -> Endpoint { let path = "/path/to/endpoint" let host = "myapi.host.com" - var queryItems: [ClientRuntime.URLQueryItem] = [] + var queryItems: [SDKURLQueryItem] = [] let endpoint: Endpoint! - queryItems.append(URLQueryItem(name: "qualifier", value: "qualifier-value")) + queryItems.append(SDKURLQueryItem(name: "qualifier", value: "qualifier-value")) endpoint = Endpoint(host: host, path: path, queryItems: queryItems, headers: headers) return endpoint } diff --git a/Tests/ClientRuntimeTests/Retry/DefaultRetryErrorInfoProviderTests.swift b/Tests/ClientRuntimeTests/Retry/DefaultRetryErrorInfoProviderTests.swift index cf9912e6d..bec71eafb 100644 --- a/Tests/ClientRuntimeTests/Retry/DefaultRetryErrorInfoProviderTests.swift +++ b/Tests/ClientRuntimeTests/Retry/DefaultRetryErrorInfoProviderTests.swift @@ -11,45 +11,114 @@ import XCTest final class DefaultRetryErrorInfoProviderTests: XCTestCase { + // MARK: - HTTPError + + func test_returnsErrorInfoWhenErrorIsARetryableHTTPStatusCode() { + + struct RetryableHTTPError: Error, HTTPError { + let httpResponse: HttpResponse + + init(statusCode: HttpStatusCode) { + self.httpResponse = HttpResponse(headers: Headers(), statusCode: statusCode) + } + } + + XCTAssertEqual( + DefaultRetryErrorInfoProvider.errorInfo(for: RetryableHTTPError(statusCode: .internalServerError)), + .init(errorType: .serverError, retryAfterHint: nil, isTimeout: false) + ) + XCTAssertEqual( + DefaultRetryErrorInfoProvider.errorInfo(for: RetryableHTTPError(statusCode: .badGateway)), + .init(errorType: .serverError, retryAfterHint: nil, isTimeout: false) + ) + XCTAssertEqual( + DefaultRetryErrorInfoProvider.errorInfo(for: RetryableHTTPError(statusCode: .serviceUnavailable)), + .init(errorType: .serverError, retryAfterHint: nil, isTimeout: false) + ) + XCTAssertEqual( + DefaultRetryErrorInfoProvider.errorInfo(for: RetryableHTTPError(statusCode: .gatewayTimeout)), + .init(errorType: .serverError, retryAfterHint: nil, isTimeout: false) + ) + } + // MARK: - Modeled errors - func test_errorInfo_returnsServerWhenErrorIsModeledRetryableAndFaultIsServer() { + func test_returnsErrorInfoWhenErrorIsModeledRetryableAndFaultIsServer() { - struct ModeledServerError: Error, ModeledError { - static var typeName: String { "ModeledServerError" } + struct ModeledRetryableServerError: Error, ModeledError { + static var typeName: String { "ModeledRetryableServerError" } static var fault: ClientRuntime.ErrorFault { .server } static var isRetryable: Bool { true } static var isThrottling: Bool { false } } - let errorInfo = DefaultRetryErrorInfoProvider.errorInfo(for: ModeledServerError()) - XCTAssertEqual(errorInfo?.errorType, .serverError) + let errorInfo = DefaultRetryErrorInfoProvider.errorInfo(for: ModeledRetryableServerError()) + XCTAssertEqual(errorInfo, .init(errorType: .serverError, retryAfterHint: nil, isTimeout: false)) } - func test_errorInfo_returnsClientWhenErrorIsModeledRetryableAndFaultIsClient() { + func test_returnsErrorInfoWhenErrorIsModeledRetryableAndFaultIsClient() { - struct ModeledClientError: Error, ModeledError { - static var typeName: String { "ModeledClientError" } + struct ModeledRetryableClientError: Error, ModeledError { + static var typeName: String { "ModeledRetryableClientError" } static var fault: ClientRuntime.ErrorFault { .client } static var isRetryable: Bool { true } static var isThrottling: Bool { false } } - let errorInfo = DefaultRetryErrorInfoProvider.errorInfo(for: ModeledClientError()) - XCTAssertEqual(errorInfo?.errorType, .clientError) + let errorInfo = DefaultRetryErrorInfoProvider.errorInfo(for: ModeledRetryableClientError()) + XCTAssertEqual(errorInfo, .init(errorType: .clientError, retryAfterHint: nil, isTimeout: false)) + } + + func test_returnsErrorInfoWhenErrorIsModeledRetryableThrottlingAndFaultIsServer() { + + struct ModeledThrottlingServerError: Error, ModeledError { + static var typeName: String { "ModeledThrottlingServerError" } + static var fault: ClientRuntime.ErrorFault { .server } + static var isRetryable: Bool { true } + static var isThrottling: Bool { true } + } + + let errorInfo = DefaultRetryErrorInfoProvider.errorInfo(for: ModeledThrottlingServerError()) + XCTAssertEqual(errorInfo, .init(errorType: .throttling, retryAfterHint: nil, isTimeout: false)) } - func test_errorInfo_returnsThrottlingWhenErrorIsModeledRetryableThrottling() { + func test_returnsErrorInfoWhenErrorIsModeledRetryableThrottlingAndFaultIsClient() { - struct ModeledThrottlingError: Error, ModeledError { - static var typeName: String { "ModeledThrottlingError" } + struct ModeledThrottlingClientError: Error, ModeledError { + static var typeName: String { "ModeledThrottlingClientError" } static var fault: ClientRuntime.ErrorFault { .server } static var isRetryable: Bool { true } static var isThrottling: Bool { true } } - let errorInfo = DefaultRetryErrorInfoProvider.errorInfo(for: ModeledThrottlingError()) - XCTAssertEqual(errorInfo?.errorType, .throttling) + let errorInfo = DefaultRetryErrorInfoProvider.errorInfo(for: ModeledThrottlingClientError()) + XCTAssertEqual(errorInfo, .init(errorType: .throttling, retryAfterHint: nil, isTimeout: false)) + } + + func test_returnsNilWhenErrorIsServerAndNotRetryableNotThrottling() { + + struct ModeledServerError: Error, ModeledError { + static var typeName: String { "ModeledServerError" } + static var fault: ClientRuntime.ErrorFault { .server } + static var isRetryable: Bool { false } + static var isThrottling: Bool { false } + } + + let errorInfo = DefaultRetryErrorInfoProvider.errorInfo(for: ModeledServerError()) + XCTAssertEqual(errorInfo, nil) + } + + func test_returnsNilWhenErrorIsClientAndNotRetryableNotThrottling() { + + struct ModeledClientError: Error, ModeledError { + static var typeName: String { "ModeledClientError" } + static var fault: ClientRuntime.ErrorFault { .client } + static var isRetryable: Bool { false } + static var isThrottling: Bool { false } + } + + let errorInfo = DefaultRetryErrorInfoProvider.errorInfo(for: ModeledClientError()) + XCTAssertEqual(errorInfo, nil) } // MARK: - Retry after hint diff --git a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift index 9d702ff96..d89f3f655 100644 --- a/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift +++ b/Tests/SmithyTestUtilTests/RequestTestUtilTests/HttpRequestTestBaseTests.swift @@ -48,10 +48,10 @@ class HttpRequestTestBaseTests: HttpRequestTestBase { Self.Context == H.Context, Self.MInput == H.Input, Self.MOutput == H.Output { - var queryItems: [ClientRuntime.URLQueryItem] = [] - var queryItem: ClientRuntime.URLQueryItem + var queryItems: [SDKURLQueryItem] = [] + var queryItem: SDKURLQueryItem if let requiredQuery = input.operationInput.requiredQuery { - queryItem = URLQueryItem(name: "RequiredQuery".urlPercentEncoding(), value: String(requiredQuery).urlPercentEncoding()) + queryItem = SDKURLQueryItem(name: "RequiredQuery".urlPercentEncoding(), value: String(requiredQuery).urlPercentEncoding()) queryItems.append(queryItem) } 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 5d04d5da7..e1fc8a69c 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 @@ -101,7 +101,7 @@ object ClientRuntimeTypes { val Date = runtimeSymbol("Date") val Data = runtimeSymbol("Data") val Document = runtimeSymbol("Document") - val URLQueryItem = runtimeSymbol("URLQueryItem") + val SDKURLQueryItem = runtimeSymbol("SDKURLQueryItem") val URL = runtimeSymbol("URL") val ModeledError = runtimeSymbol("ModeledError") val UnknownClientError = runtimeSymbol("ClientError.unknownError") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputHeadersMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputHeadersMiddleware.kt index b29e713a0..26fab3522 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputHeadersMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputHeadersMiddleware.kt @@ -28,11 +28,18 @@ class OperationInputHeadersMiddleware( op: OperationShape, operationStackName: String, ) { + if (!MiddlewareShapeUtils.hasHttpHeaders(model, op)) { return } val inputShapeName = MiddlewareShapeUtils.inputSymbol(symbolProvider, model, op).name val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name - val hasHeaders = MiddlewareShapeUtils.hasHttpHeaders(model, op) - if (hasHeaders) { - writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<$inputShapeName, $outputShapeName>())", ClientRuntimeTypes.Middleware.HeaderMiddleware) - } + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L>(\$L.headerProvider(_:)))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.HeaderMiddleware, + inputShapeName, + outputShapeName, + inputShapeName, + ) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputQueryItemMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputQueryItemMiddleware.kt index fb791dcfc..12fec9221 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputQueryItemMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputQueryItemMiddleware.kt @@ -32,7 +32,16 @@ class OperationInputQueryItemMiddleware( val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name val hasQueryItems = MiddlewareShapeUtils.hasQueryItems(model, op) if (hasQueryItems) { - writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<$inputShapeName, $outputShapeName>())", ClientRuntimeTypes.Middleware.QueryItemMiddleware) + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L>(\$L.queryItemProvider(_:)))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.QueryItemMiddleware, + inputShapeName, + outputShapeName, + inputShapeName, + ) } } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt index c94c46eff..c5afcf70e 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/OperationInputUrlPathMiddleware.kt @@ -31,6 +31,17 @@ class OperationInputUrlPathMiddleware( ) { val inputShapeName = MiddlewareShapeUtils.inputSymbol(symbolProvider, model, op).name val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name - writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<$inputShapeName, $outputShapeName>($inputParameters))", ClientRuntimeTypes.Middleware.URLPathMiddleware) + val params = "".takeIf { inputParameters.isEmpty() } ?: "$inputParameters, " + writer.write( + "\$L.\$L.intercept(position: \$L, middleware: \$N<\$L, \$L>(\$L\$L.urlPathProvider(_:)))", + operationStackName, + middlewareStep.stringValue(), + position.stringValue(), + ClientRuntimeTypes.Middleware.URLPathMiddleware, + inputShapeName, + outputShapeName, + params, + inputShapeName, + ) } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt index 0fec462d2..f57c97949 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpHeaderProvider.kt @@ -66,8 +66,14 @@ class HttpHeaderProvider( } fun renderProvider(writer: SwiftWriter) { - writer.openBlock("extension \$N: \$N {", "}", inputSymbol, ClientRuntimeTypes.Middleware.Providers.HeaderProvider) { - writer.openBlock("public var headers: \$N {", "}", ClientRuntimeTypes.Http.Headers) { + writer.openBlock("extension \$N {", "}", inputSymbol) { + writer.write("") + writer.openBlock( + "static func headerProvider(_ value: \$N) -> \$N {", + "}", + inputSymbol, + ClientRuntimeTypes.Http.Headers, + ) { writer.write("var items = \$N()", ClientRuntimeTypes.Http.Headers) generateHeaders() generatePrefixHeaders() @@ -83,7 +89,7 @@ class HttpHeaderProvider( val paramName = it.locationName val isBoxed = ctx.symbolProvider.toSymbol(it.member).isBoxed() if (isBoxed) { - writer.openBlock("if let $memberName = $memberName {", "}") { + writer.openBlock("if let $memberName = value.$memberName {", "}") { if (memberTarget is CollectionShape) { writer.openBlock("$memberName.forEach { headerValue in ", "}") { renderHeader(memberTarget.member, "headerValue", paramName, true) @@ -129,7 +135,7 @@ class HttpHeaderProvider( val memberTarget = ctx.model.expectShape(it.member.target) val paramName = it.locationName - writer.openBlock("if let $memberName = $memberName {", "}") { + writer.openBlock("if let $memberName = value.$memberName {", "}") { val mapValueShape = memberTarget.asMapShape().get().value val mapValueShapeTarget = ctx.model.expectShape(mapValueShape.target) val mapValueShapeTargetSymbol = ctx.symbolProvider.toSymbol(mapValueShapeTarget) diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt index 90b0d5c24..855763e4d 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpQueryItemProvider.kt @@ -70,10 +70,18 @@ class HttpQueryItemProvider( } fun renderProvider(writer: SwiftWriter) { - writer.openBlock("extension \$N: \$N {", "}", inputSymbol, ClientRuntimeTypes.Middleware.Providers.QueryItemProvider) { - writer.openBlock("public var queryItems: [\$N] {", "}", ClientRuntimeTypes.Core.URLQueryItem) { - writer.openBlock("get throws {", "}") { - writer.write("var items = [\$N]()", ClientRuntimeTypes.Core.URLQueryItem) + writer.openBlock("extension \$N {", "}", inputSymbol) { + writer.write("") + writer.openBlock( + "static func queryItemProvider(_ value: \$N) throws -> [\$N] {", + "}", + inputSymbol, + ClientRuntimeTypes.Core.SDKURLQueryItem + ) { + if (queryLiterals.isEmpty() && queryBindings.isEmpty()) { + writer.write("return []") + } else { + writer.write("var items = [\$N]()", ClientRuntimeTypes.Core.SDKURLQueryItem) generateQueryItems() writer.write("return items") } @@ -84,7 +92,7 @@ class HttpQueryItemProvider( private fun generateQueryItems() { queryLiterals.forEach { (queryItemKey, queryItemValue) -> val queryValue = if (queryItemValue.isBlank()) "nil" else "\"${queryItemValue}\"" - writer.write("items.append(\$N(name: \$S, value: \$L))", ClientRuntimeTypes.Core.URLQueryItem, queryItemKey, queryValue) + writer.write("items.append(\$N(name: \$S, value: \$L))", ClientRuntimeTypes.Core.SDKURLQueryItem, queryItemKey, queryValue) } var httpQueryParamBinding: HttpBindingDescriptor? = null @@ -111,7 +119,7 @@ class HttpQueryItemProvider( } private fun renderHttpQueryParamMap(memberTarget: MapShape, memberName: String) { - writer.openBlock("if let $memberName = $memberName {", "}") { + writer.openBlock("if let $memberName = value.$memberName {", "}") { val currentQueryItemsNames = "currentQueryItemNames" writer.write("let $currentQueryItemsNames = items.map({\$L.name})", "\$0") @@ -121,13 +129,13 @@ class HttpQueryItemProvider( writer.openBlock("if !$currentQueryItemsNames.contains(key0) {", "}") { val suffix = if (memberTarget.hasTrait()) "?" else "" writer.openBlock("value0$suffix.forEach { value1 in", "}") { - writer.write("let queryItem = \$N(name: key0.urlPercentEncoding(), value: value1.urlPercentEncoding())", ClientRuntimeTypes.Core.URLQueryItem) + writer.write("let queryItem = \$N(name: key0.urlPercentEncoding(), value: value1.urlPercentEncoding())", ClientRuntimeTypes.Core.SDKURLQueryItem) writer.write("items.append(queryItem)") } } } else { writer.openBlock("if !$currentQueryItemsNames.contains(key0) {", "}") { - writer.write("let queryItem = \$N(name: key0.urlPercentEncoding(), value: value0.urlPercentEncoding())", ClientRuntimeTypes.Core.URLQueryItem) + writer.write("let queryItem = \$N(name: key0.urlPercentEncoding(), value: value0.urlPercentEncoding())", ClientRuntimeTypes.Core.SDKURLQueryItem) writer.write("items.append(queryItem)") } } @@ -138,7 +146,7 @@ class HttpQueryItemProvider( fun renderHttpQuery(queryBinding: HttpBindingDescriptor, memberName: String, memberTarget: Shape, paramName: String, bindingIndex: HttpBindingIndex, isBoxed: Boolean) { if (isBoxed) { if (queryBinding.member.isRequired()) { - writer.openBlock("guard let \$L = \$L else {", "}", memberName, memberName) { + writer.openBlock("guard let \$L = value.\$L else {", "}", memberName, memberName) { writer.write( "let message = \"Creating a URL Query Item failed. \$L is required and must not be nil.\"", memberName @@ -148,14 +156,14 @@ class HttpQueryItemProvider( if (memberTarget is CollectionShape) { renderListOrSet(memberTarget, bindingIndex, memberName, paramName) } else { - renderQueryItem(queryBinding.member, bindingIndex, memberName, paramName) + renderQueryItem(queryBinding.member, bindingIndex, memberName, paramName, true) } } else { - writer.openBlock("if let $memberName = $memberName {", "}") { + writer.openBlock("if let $memberName = value.$memberName {", "}") { if (memberTarget is CollectionShape) { renderListOrSet(memberTarget, bindingIndex, memberName, paramName) } else { - renderQueryItem(queryBinding.member, bindingIndex, memberName, paramName) + renderQueryItem(queryBinding.member, bindingIndex, memberName, paramName, true) } } } @@ -163,12 +171,12 @@ class HttpQueryItemProvider( if (memberTarget is CollectionShape) { renderListOrSet(memberTarget, bindingIndex, memberName, paramName) } else { - renderQueryItem(queryBinding.member, bindingIndex, memberName, paramName) + renderQueryItem(queryBinding.member, bindingIndex, memberName, paramName, false) } } } - private fun renderQueryItem(member: MemberShape, bindingIndex: HttpBindingIndex, originalMemberName: String, paramName: String) { + private fun renderQueryItem(member: MemberShape, bindingIndex: HttpBindingIndex, originalMemberName: String, paramName: String, unwrapped: Boolean) { var (memberName, requiresDoCatch) = formatHeaderOrQueryValue( ctx, originalMemberName, @@ -180,15 +188,16 @@ class HttpQueryItemProvider( if (requiresDoCatch) { renderDoCatch(memberName, paramName) } else { + val prefix = "".takeIf { unwrapped } ?: "value." if (member.needsDefaultValueCheck(ctx.model, ctx.symbolProvider)) { - writer.openBlock("if $memberName != ${member.defaultValue(ctx.symbolProvider)} {", "}") { + writer.openBlock("if value.$memberName != ${member.defaultValue(ctx.symbolProvider)} {", "}") { val queryItemName = "${ctx.symbolProvider.toMemberNames(member).second}QueryItem" - writer.write("let $queryItemName = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$N($memberName).urlPercentEncoding())", ClientRuntimeTypes.Core.URLQueryItem, SwiftTypes.String) + writer.write("let $queryItemName = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$N($prefix$memberName).urlPercentEncoding())", ClientRuntimeTypes.Core.SDKURLQueryItem, SwiftTypes.String) writer.write("items.append($queryItemName)") } } else { val queryItemName = "${ctx.symbolProvider.toMemberNames(member).second}QueryItem" - writer.write("let $queryItemName = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$N($memberName).urlPercentEncoding())", ClientRuntimeTypes.Core.URLQueryItem, SwiftTypes.String) + writer.write("let $queryItemName = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$N($prefix$memberName).urlPercentEncoding())", ClientRuntimeTypes.Core.SDKURLQueryItem, SwiftTypes.String) writer.write("items.append($queryItemName)") } } @@ -213,7 +222,7 @@ class HttpQueryItemProvider( if (requiresDoCatch) { renderDoCatch(queryItemValue, paramName) } else { - writer.write("let queryItem = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$N($queryItemValue).urlPercentEncoding())", ClientRuntimeTypes.Core.URLQueryItem, SwiftTypes.String) + writer.write("let queryItem = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$N($queryItemValue).urlPercentEncoding())", ClientRuntimeTypes.Core.SDKURLQueryItem, SwiftTypes.String) writer.write("items.append(queryItem)") } } @@ -222,7 +231,7 @@ class HttpQueryItemProvider( private fun renderDoCatch(queryItemValueWithExtension: String, paramName: String) { writer.openBlock("do {", "} catch let err {") { writer.write("let base64EncodedValue = $queryItemValueWithExtension") - writer.write("let queryItem = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$N($queryItemValueWithExtension).urlPercentEncoding())", ClientRuntimeTypes.Core.URLQueryItem, SwiftTypes.String) + writer.write("let queryItem = \$N(name: \"$paramName\".urlPercentEncoding(), value: \$N($queryItemValueWithExtension).urlPercentEncoding())", ClientRuntimeTypes.Core.SDKURLQueryItem, SwiftTypes.String) writer.write("items.append(queryItem)") } writer.write("}") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpUrlPathProvider.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpUrlPathProvider.kt index c19ac05c6..b15e348d3 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpUrlPathProvider.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/providers/HttpUrlPathProvider.kt @@ -9,7 +9,6 @@ import software.amazon.smithy.model.shapes.ShapeType import software.amazon.smithy.model.traits.EnumTrait import software.amazon.smithy.model.traits.HttpTrait import software.amazon.smithy.model.traits.TimestampFormatTrait -import software.amazon.smithy.swift.codegen.ClientRuntimeTypes import software.amazon.smithy.swift.codegen.SwiftDependency import software.amazon.smithy.swift.codegen.SwiftTypes import software.amazon.smithy.swift.codegen.SwiftWriter @@ -49,8 +48,14 @@ class HttpUrlPathProvider( } fun renderProvider(writer: SwiftWriter) { - writer.openBlock("extension \$N: \$N {", "}", inputSymbol, ClientRuntimeTypes.Middleware.Providers.URLPathProvider) { - writer.openBlock("public var urlPath: \$T {", "}", SwiftTypes.String) { + writer.openBlock("extension \$N {", "}", inputSymbol) { + writer.write("") + writer.openBlock( + "static func urlPathProvider(_ value: \$N) -> \$T {", + "}", + inputSymbol, + SwiftTypes.String, + ) { renderUriPath() } } @@ -102,7 +107,7 @@ class HttpUrlPathProvider( // unwrap the label members if boxed if (symbol.isBoxed()) { - writer.openBlock("guard let $labelMemberName = $labelMemberName else {", "}") { + writer.openBlock("guard let $labelMemberName = value.$labelMemberName else {", "}") { writer.write("return nil") } } diff --git a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt index 763fc3d27..e6ed992de 100644 --- a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt @@ -24,7 +24,7 @@ class ContentMd5MiddlewareTests { .build() var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(IdempotencyTokenWithStructureInput.urlPathProvider(_:))) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.ContentMD5Middleware()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) diff --git a/smithy-swift-codegen/src/test/kotlin/HttpHeaderProviderGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpHeaderProviderGeneratorTests.kt index b52e317bd..a64674bf4 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpHeaderProviderGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpHeaderProviderGeneratorTests.kt @@ -23,21 +23,21 @@ class HttpHeaderProviderGeneratorTests { fun `it creates smoke test request builder`() { val contents = getModelFileContents("example", "SmokeTestInput+HeaderProvider.swift", newTestContext.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension SmokeTestInput: ClientRuntime.HeaderProvider { - public var headers: ClientRuntime.Headers { - var items = ClientRuntime.Headers() - if let header1 = header1 { - items.add(Header(name: "X-Header1", value: Swift.String(header1))) - } - if let header2 = header2 { - items.add(Header(name: "X-Header2", value: Swift.String(header2))) - } - return items - } - } - """.trimIndent() + val expectedContents = """ +extension SmokeTestInput { + + static func headerProvider(_ value: SmokeTestInput) -> ClientRuntime.Headers { + var items = ClientRuntime.Headers() + if let header1 = value.header1 { + items.add(Header(name: "X-Header1", value: Swift.String(header1))) + } + if let header2 = value.header2 { + items.add(Header(name: "X-Header2", value: Swift.String(header2))) + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -45,18 +45,18 @@ class HttpHeaderProviderGeneratorTests { fun `it builds headers with enums as raw values`() { val contents = getModelFileContents("example", "EnumInputInput+HeaderProvider.swift", newTestContext.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension EnumInputInput: ClientRuntime.HeaderProvider { - public var headers: ClientRuntime.Headers { - var items = ClientRuntime.Headers() - if let enumHeader = enumHeader { - items.add(Header(name: "X-EnumHeader", value: Swift.String(enumHeader.rawValue))) - } - return items - } - } - """.trimIndent() + val expectedContents = """ +extension EnumInputInput { + + static func headerProvider(_ value: EnumInputInput) -> ClientRuntime.Headers { + var items = ClientRuntime.Headers() + if let enumHeader = value.enumHeader { + items.add(Header(name: "X-EnumHeader", value: Swift.String(enumHeader.rawValue))) + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -68,18 +68,18 @@ class HttpHeaderProviderGeneratorTests { newTestContext.manifest ) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension IdempotencyTokenWithoutHttpPayloadTraitOnTokenInput: ClientRuntime.HeaderProvider { - public var headers: ClientRuntime.Headers { - var items = ClientRuntime.Headers() - if let token = token { - items.add(Header(name: "token", value: Swift.String(token))) - } - return items - } - } - """.trimIndent() + val expectedContents = """ +extension IdempotencyTokenWithoutHttpPayloadTraitOnTokenInput { + + static func headerProvider(_ value: IdempotencyTokenWithoutHttpPayloadTraitOnTokenInput) -> ClientRuntime.Headers { + var items = ClientRuntime.Headers() + if let token = value.token { + items.add(Header(name: "token", value: Swift.String(token))) + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -88,21 +88,21 @@ class HttpHeaderProviderGeneratorTests { val contents = getModelFileContents("example", "TimestampInputInput+HeaderProvider.swift", newTestContext.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension TimestampInputInput: ClientRuntime.HeaderProvider { - public var headers: ClientRuntime.Headers { - var items = ClientRuntime.Headers() - if let headerEpoch = headerEpoch { - items.add(Header(name: "X-Epoch", value: Swift.String(TimestampFormatter(format: .epochSeconds).string(from: headerEpoch)))) - } - if let headerHttpDate = headerHttpDate { - items.add(Header(name: "X-Date", value: Swift.String(TimestampFormatter(format: .httpDate).string(from: headerHttpDate)))) - } - return items - } - } - """.trimIndent() + val expectedContents = """ +extension TimestampInputInput { + + static func headerProvider(_ value: TimestampInputInput) -> ClientRuntime.Headers { + var items = ClientRuntime.Headers() + if let headerEpoch = value.headerEpoch { + items.add(Header(name: "X-Epoch", value: Swift.String(TimestampFormatter(format: .epochSeconds).string(from: headerEpoch)))) + } + if let headerHttpDate = value.headerHttpDate { + items.add(Header(name: "X-Date", value: Swift.String(TimestampFormatter(format: .httpDate).string(from: headerHttpDate)))) + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index 20c0135ba..03d2b40e0 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -2,12 +2,9 @@ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: Apache-2.0. */ - import io.kotest.matchers.string.shouldContainOnlyOnce import org.junit.jupiter.api.Test - class HttpProtocolClientGeneratorTests { - @Test fun `it renders client initialization block`() { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") @@ -75,7 +72,6 @@ class HttpProtocolClientGeneratorTests { """.trimIndent() ) } - @Test fun `it renders host prefix with label in context correctly`() { val context = setupTests("host-prefix-operation.smithy", "com.test#Example") @@ -93,7 +89,6 @@ class HttpProtocolClientGeneratorTests { """ contents.shouldContainOnlyOnce(expectedFragment) } - @Test fun `it renders operation implementations in extension`() { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") @@ -101,7 +96,6 @@ class HttpProtocolClientGeneratorTests { contents.shouldSyntacticSanityCheck() contents.shouldContainOnlyOnce("extension RestJsonProtocolClient: RestJsonProtocolClientProtocol {") } - @Test fun `it renders an operation body`() { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") @@ -125,7 +119,7 @@ class HttpProtocolClientGeneratorTests { .build() var operation = ClientRuntime.OperationStack(id: "allocateWidget") operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.clientToken)) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(AllocateWidgetInput.urlPathProvider(_:))) 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")) @@ -141,7 +135,6 @@ 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") @@ -164,7 +157,7 @@ class HttpProtocolClientGeneratorTests { .withUnsignedPayloadTrait(value: true) .build() var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStream") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(UnsignedFooBlobStreamInput.urlPathProvider(_:))) 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")) @@ -180,7 +173,6 @@ class HttpProtocolClientGeneratorTests { """ 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") @@ -203,7 +195,7 @@ class HttpProtocolClientGeneratorTests { .withUnsignedPayloadTrait(value: true) .build() var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(UnsignedFooBlobStreamWithLengthInput.urlPathProvider(_:))) 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")) @@ -219,7 +211,6 @@ class HttpProtocolClientGeneratorTests { """ 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") @@ -242,7 +233,7 @@ class HttpProtocolClientGeneratorTests { .withUnsignedPayloadTrait(value: true) .build() var operation = ClientRuntime.OperationStack(id: "unsignedFooBlobStreamWithLength") - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(UnsignedFooBlobStreamWithLengthInput.urlPathProvider(_:))) 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")) @@ -258,7 +249,6 @@ class HttpProtocolClientGeneratorTests { """ 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/HttpProtocolUnitTestRequestGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt index cc8a41d03..8a4457262 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolUnitTestRequestGeneratorTests.kt @@ -82,7 +82,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .post) .build() var operationStack = OperationStack(id: "SmokeTest") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, SmokeTestInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -91,8 +91,8 @@ class HttpProtocolUnitTestRequestGeneratorTests { input.withHost(host) return try await next.handle(context: context, input: input) } - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware()) - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.QueryItemMiddleware()) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware(SmokeTestInput.headerProvider(_:))) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.QueryItemMiddleware(SmokeTestInput.queryItemProvider(_:))) operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) @@ -135,8 +135,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { fun `it creates explicit string test`() { val contents = getTestFileContents("example", "ExplicitStringRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testExplicitString() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -170,7 +169,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .post) .build() var operationStack = OperationStack(id: "ExplicitString") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, ExplicitStringInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -212,8 +211,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { fun `it creates a unit test for a request without a body`() { val contents = getTestFileContents("example", "EmptyInputAndEmptyOutputRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testRestJsonEmptyInputAndEmptyOutput() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -239,7 +237,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .post) .build() var operationStack = OperationStack(id: "RestJsonEmptyInputAndEmptyOutput") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, EmptyInputAndEmptyOutputInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -272,8 +270,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { fun `it creates a unit test for a request without a body given an empty object`() { val contents = getTestFileContents("example", "SimpleScalarPropertiesRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testRestJsonDoesntSerializeNullStructureValues() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -305,7 +302,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .put) .build() var operationStack = OperationStack(id: "RestJsonDoesntSerializeNullStructureValues") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, SimpleScalarPropertiesInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -314,7 +311,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { input.withHost(host) return try await next.handle(context: context, input: input) } - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware()) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware(SimpleScalarPropertiesInput.headerProvider(_:))) operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/json")) operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.BodyMiddleware(documentWritingClosure: ClientRuntime.JSONReadWrite.documentWritingClosure(encoder: encoder), inputWritingClosure: JSONReadWrite.writingClosure())) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) @@ -396,7 +393,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .post) .build() var operationStack = OperationStack(id: "RestJsonStreamingTraitsWithBlob") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, StreamingTraitsInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -405,7 +402,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { input.withHost(host) return try await next.handle(context: context, input: input) } - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware()) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware(StreamingTraitsInput.headerProvider(_:))) operationStack.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/octet-stream")) operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.BlobStreamBodyMiddleware(keyPath: \.blob)) operationStack.finalizeStep.intercept(position: .before, middleware: ClientRuntime.ContentLengthMiddleware()) @@ -439,8 +436,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { fun `it creates unit test with an empty map`() { val contents = getTestFileContents("example", "HttpPrefixHeadersRequestTest.swift", ctx.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ + val expectedContents = """ func testRestJsonHttpPrefixHeadersAreNotPresent() async throws { let urlPrefix = urlPrefixFromHost(host: "") let hostOnly = hostOnlyFromHost(host: "") @@ -472,7 +468,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .get) .build() var operationStack = OperationStack(id: "RestJsonHttpPrefixHeadersAreNotPresent") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, HttpPrefixHeadersInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -481,7 +477,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { input.withHost(host) return try await next.handle(context: context, input: input) } - operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware()) + operationStack.serializeStep.intercept(position: .after, middleware: ClientRuntime.HeaderMiddleware(HttpPrefixHeadersInput.headerProvider(_:))) operationStack.deserializeStep.intercept(position: .after, middleware: MockDeserializeMiddleware( id: "TestDeserializeMiddleware"){ context, actual in @@ -543,7 +539,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .put) .build() var operationStack = OperationStack(id: "RestJsonSerializeStringUnionValue") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, JsonUnionsInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -647,7 +643,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .put) .build() var operationStack = OperationStack(id: "RestJsonRecursiveShapes") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, RecursiveShapesInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -739,7 +735,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .put) .build() var operationStack = OperationStack(id: "InlineDocumentInput") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, InlineDocumentInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) @@ -829,7 +825,7 @@ class HttpProtocolUnitTestRequestGeneratorTests { .withMethod(value: .put) .build() var operationStack = OperationStack(id: "InlineDocumentAsPayloadInput") - operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix)) + operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(urlPrefix: urlPrefix, InlineDocumentAsPayloadInput.urlPathProvider(_:))) operationStack.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware(host: hostOnly)) operationStack.buildStep.intercept(position: .after, id: "RequestTestEndpointResolver") { (context, input, next) -> ClientRuntime.OperationOutput in input.withMethod(context.getMethod()) diff --git a/smithy-swift-codegen/src/test/kotlin/HttpQueryItemProviderGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpQueryItemProviderGeneratorTests.kt index 0dc45810e..28dea5baa 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpQueryItemProviderGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpQueryItemProviderGeneratorTests.kt @@ -15,21 +15,19 @@ class HttpQueryItemProviderGeneratorTests { val contents = getModelFileContents("example", "QueryIdempotencyTokenAutoFillInput+QueryItemProvider.swift", context.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension QueryIdempotencyTokenAutoFillInput: ClientRuntime.QueryItemProvider { - public var queryItems: [ClientRuntime.URLQueryItem] { - get throws { - var items = [ClientRuntime.URLQueryItem]() - if let token = token { - let tokenQueryItem = ClientRuntime.URLQueryItem(name: "token".urlPercentEncoding(), value: Swift.String(token).urlPercentEncoding()) - items.append(tokenQueryItem) - } - return items - } - } - } - """.trimIndent() + val expectedContents = """ +extension QueryIdempotencyTokenAutoFillInput { + + static func queryItemProvider(_ value: QueryIdempotencyTokenAutoFillInput) throws -> [ClientRuntime.SDKURLQueryItem] { + var items = [ClientRuntime.SDKURLQueryItem]() + if let token = value.token { + let tokenQueryItem = ClientRuntime.SDKURLQueryItem(name: "token".urlPercentEncoding(), value: Swift.String(token).urlPercentEncoding()) + items.append(tokenQueryItem) + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -38,27 +36,25 @@ class HttpQueryItemProviderGeneratorTests { val context = setupTests("http-binding-protocol-generator-test.smithy", "com.test#Example") val contents = getModelFileContents("example", "TimestampInputInput+QueryItemProvider.swift", context.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension TimestampInputInput: ClientRuntime.QueryItemProvider { - public var queryItems: [ClientRuntime.URLQueryItem] { - get throws { - var items = [ClientRuntime.URLQueryItem]() - if let queryTimestamp = queryTimestamp { - let queryTimestampQueryItem = ClientRuntime.URLQueryItem(name: "qtime".urlPercentEncoding(), value: Swift.String(TimestampFormatter(format: .dateTime).string(from: queryTimestamp)).urlPercentEncoding()) - items.append(queryTimestampQueryItem) - } - if let queryTimestampList = queryTimestampList { - queryTimestampList.forEach { queryItemValue in - let queryItem = ClientRuntime.URLQueryItem(name: "qtimeList".urlPercentEncoding(), value: Swift.String(TimestampFormatter(format: .dateTime).string(from: queryItemValue)).urlPercentEncoding()) - items.append(queryItem) - } - } - return items - } - } + val expectedContents = """ +extension TimestampInputInput { + + static func queryItemProvider(_ value: TimestampInputInput) throws -> [ClientRuntime.SDKURLQueryItem] { + var items = [ClientRuntime.SDKURLQueryItem]() + if let queryTimestamp = value.queryTimestamp { + let queryTimestampQueryItem = ClientRuntime.SDKURLQueryItem(name: "qtime".urlPercentEncoding(), value: Swift.String(TimestampFormatter(format: .dateTime).string(from: queryTimestamp)).urlPercentEncoding()) + items.append(queryTimestampQueryItem) + } + if let queryTimestampList = value.queryTimestampList { + queryTimestampList.forEach { queryItemValue in + let queryItem = ClientRuntime.SDKURLQueryItem(name: "qtimeList".urlPercentEncoding(), value: Swift.String(TimestampFormatter(format: .dateTime).string(from: queryItemValue)).urlPercentEncoding()) + items.append(queryItem) } - """.trimIndent() + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -67,21 +63,19 @@ class HttpQueryItemProviderGeneratorTests { val context = setupTests("http-binding-protocol-generator-test.smithy", "com.test#Example") val contents = getModelFileContents("example", "SmokeTestInput+QueryItemProvider.swift", context.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension SmokeTestInput: ClientRuntime.QueryItemProvider { - public var queryItems: [ClientRuntime.URLQueryItem] { - get throws { - var items = [ClientRuntime.URLQueryItem]() - if let query1 = query1 { - let query1QueryItem = ClientRuntime.URLQueryItem(name: "Query1".urlPercentEncoding(), value: Swift.String(query1).urlPercentEncoding()) - items.append(query1QueryItem) - } - return items - } - } - } - """.trimIndent() + val expectedContents = """ +extension SmokeTestInput { + + static func queryItemProvider(_ value: SmokeTestInput) throws -> [ClientRuntime.SDKURLQueryItem] { + var items = [ClientRuntime.SDKURLQueryItem]() + if let query1 = value.query1 { + let query1QueryItem = ClientRuntime.SDKURLQueryItem(name: "Query1".urlPercentEncoding(), value: Swift.String(query1).urlPercentEncoding()) + items.append(query1QueryItem) + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -99,36 +93,34 @@ class HttpQueryItemProviderGeneratorTests { val context = setupTests("http-query-params-stringmap.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/example/models/AllQueryStringTypesInput+QueryItemProvider.swift") contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension AllQueryStringTypesInput: ClientRuntime.QueryItemProvider { - public var queryItems: [ClientRuntime.URLQueryItem] { - get throws { - var items = [ClientRuntime.URLQueryItem]() - if let queryStringList = queryStringList { - queryStringList.forEach { queryItemValue in - let queryItem = ClientRuntime.URLQueryItem(name: "StringList".urlPercentEncoding(), value: Swift.String(queryItemValue).urlPercentEncoding()) - items.append(queryItem) - } - } - if let queryString = queryString { - let queryStringQueryItem = ClientRuntime.URLQueryItem(name: "String".urlPercentEncoding(), value: Swift.String(queryString).urlPercentEncoding()) - items.append(queryStringQueryItem) - } - if let queryParamsMapOfStrings = queryParamsMapOfStrings { - let currentQueryItemNames = items.map({${'$'}0.name}) - queryParamsMapOfStrings.forEach { key0, value0 in - if !currentQueryItemNames.contains(key0) { - let queryItem = ClientRuntime.URLQueryItem(name: key0.urlPercentEncoding(), value: value0.urlPercentEncoding()) - items.append(queryItem) - } - } - } - return items - } + val expectedContents = """ +extension AllQueryStringTypesInput { + + static func queryItemProvider(_ value: AllQueryStringTypesInput) throws -> [ClientRuntime.SDKURLQueryItem] { + var items = [ClientRuntime.SDKURLQueryItem]() + if let queryStringList = value.queryStringList { + queryStringList.forEach { queryItemValue in + let queryItem = ClientRuntime.SDKURLQueryItem(name: "StringList".urlPercentEncoding(), value: Swift.String(queryItemValue).urlPercentEncoding()) + items.append(queryItem) + } + } + if let queryString = value.queryString { + let queryStringQueryItem = ClientRuntime.SDKURLQueryItem(name: "String".urlPercentEncoding(), value: Swift.String(queryString).urlPercentEncoding()) + items.append(queryStringQueryItem) + } + if let queryParamsMapOfStrings = value.queryParamsMapOfStrings { + let currentQueryItemNames = items.map({$0.name}) + queryParamsMapOfStrings.forEach { key0, value0 in + if !currentQueryItemNames.contains(key0) { + let queryItem = ClientRuntime.SDKURLQueryItem(name: key0.urlPercentEncoding(), value: value0.urlPercentEncoding()) + items.append(queryItem) } } - """.trimIndent() + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -137,32 +129,30 @@ class HttpQueryItemProviderGeneratorTests { val context = setupTests("http-query-params-stringlistmap.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/example/models/QueryParamsAsStringListMapInput+QueryItemProvider.swift") contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension QueryParamsAsStringListMapInput: ClientRuntime.QueryItemProvider { - public var queryItems: [ClientRuntime.URLQueryItem] { - get throws { - var items = [ClientRuntime.URLQueryItem]() - if let qux = qux { - let quxQueryItem = ClientRuntime.URLQueryItem(name: "corge".urlPercentEncoding(), value: Swift.String(qux).urlPercentEncoding()) - items.append(quxQueryItem) - } - if let foo = foo { - let currentQueryItemNames = items.map({${'$'}0.name}) - foo.forEach { key0, value0 in - if !currentQueryItemNames.contains(key0) { - value0.forEach { value1 in - let queryItem = ClientRuntime.URLQueryItem(name: key0.urlPercentEncoding(), value: value1.urlPercentEncoding()) - items.append(queryItem) - } - } - } - } - return items + val expectedContents = """ +extension QueryParamsAsStringListMapInput { + + static func queryItemProvider(_ value: QueryParamsAsStringListMapInput) throws -> [ClientRuntime.SDKURLQueryItem] { + var items = [ClientRuntime.SDKURLQueryItem]() + if let qux = value.qux { + let quxQueryItem = ClientRuntime.SDKURLQueryItem(name: "corge".urlPercentEncoding(), value: Swift.String(qux).urlPercentEncoding()) + items.append(quxQueryItem) + } + if let foo = value.foo { + let currentQueryItemNames = items.map({$0.name}) + foo.forEach { key0, value0 in + if !currentQueryItemNames.contains(key0) { + value0.forEach { value1 in + let queryItem = ClientRuntime.SDKURLQueryItem(name: key0.urlPercentEncoding(), value: value1.urlPercentEncoding()) + items.append(queryItem) } } } - """.trimIndent() + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -171,30 +161,28 @@ class HttpQueryItemProviderGeneratorTests { val context = setupTests("http-query-params-precedence.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/example/models/QueryPrecedenceInput+QueryItemProvider.swift") contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension QueryPrecedenceInput: ClientRuntime.QueryItemProvider { - public var queryItems: [ClientRuntime.URLQueryItem] { - get throws { - var items = [ClientRuntime.URLQueryItem]() - if let foo = foo { - let fooQueryItem = ClientRuntime.URLQueryItem(name: "bar".urlPercentEncoding(), value: Swift.String(foo).urlPercentEncoding()) - items.append(fooQueryItem) - } - if let baz = baz { - let currentQueryItemNames = items.map({${'$'}0.name}) - baz.forEach { key0, value0 in - if !currentQueryItemNames.contains(key0) { - let queryItem = ClientRuntime.URLQueryItem(name: key0.urlPercentEncoding(), value: value0.urlPercentEncoding()) - items.append(queryItem) - } - } - } - return items - } + val expectedContents = """ +extension QueryPrecedenceInput { + + static func queryItemProvider(_ value: QueryPrecedenceInput) throws -> [ClientRuntime.SDKURLQueryItem] { + var items = [ClientRuntime.SDKURLQueryItem]() + if let foo = value.foo { + let fooQueryItem = ClientRuntime.SDKURLQueryItem(name: "bar".urlPercentEncoding(), value: Swift.String(foo).urlPercentEncoding()) + items.append(fooQueryItem) + } + if let baz = value.baz { + let currentQueryItemNames = items.map({${'$'}0.name}) + baz.forEach { key0, value0 in + if !currentQueryItemNames.contains(key0) { + let queryItem = ClientRuntime.SDKURLQueryItem(name: key0.urlPercentEncoding(), value: value0.urlPercentEncoding()) + items.append(queryItem) } } - """.trimIndent() + } + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -203,37 +191,35 @@ class HttpQueryItemProviderGeneratorTests { val context = setupTests("http-binding-protocol-generator-test.smithy", "com.test#Example") val contents = getModelFileContents("example", "RequiredHttpFieldsInput+QueryItemProvider.swift", context.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension RequiredHttpFieldsInput: ClientRuntime.QueryItemProvider { - public var queryItems: [ClientRuntime.URLQueryItem] { - get throws { - var items = [ClientRuntime.URLQueryItem]() - guard let query1 = query1 else { - let message = "Creating a URL Query Item failed. query1 is required and must not be nil." - throw ClientRuntime.ClientError.unknownError(message) - } - let query1QueryItem = ClientRuntime.URLQueryItem(name: "Query1".urlPercentEncoding(), value: Swift.String(query1).urlPercentEncoding()) - items.append(query1QueryItem) - guard let query2 = query2 else { - let message = "Creating a URL Query Item failed. query2 is required and must not be nil." - throw ClientRuntime.ClientError.unknownError(message) - } - query2.forEach { queryItemValue in - let queryItem = ClientRuntime.URLQueryItem(name: "Query2".urlPercentEncoding(), value: Swift.String(TimestampFormatter(format: .dateTime).string(from: queryItemValue)).urlPercentEncoding()) - items.append(queryItem) - } - guard let query3 = query3 else { - let message = "Creating a URL Query Item failed. query3 is required and must not be nil." - throw ClientRuntime.ClientError.unknownError(message) - } - let query3QueryItem = ClientRuntime.URLQueryItem(name: "Query3".urlPercentEncoding(), value: Swift.String(query3).urlPercentEncoding()) - items.append(query3QueryItem) - return items - } - } - } - """.trimIndent() + val expectedContents = """ +extension RequiredHttpFieldsInput { + + static func queryItemProvider(_ value: RequiredHttpFieldsInput) throws -> [ClientRuntime.SDKURLQueryItem] { + var items = [ClientRuntime.SDKURLQueryItem]() + guard let query1 = value.query1 else { + let message = "Creating a URL Query Item failed. query1 is required and must not be nil." + throw ClientRuntime.ClientError.unknownError(message) + } + let query1QueryItem = ClientRuntime.SDKURLQueryItem(name: "Query1".urlPercentEncoding(), value: Swift.String(query1).urlPercentEncoding()) + items.append(query1QueryItem) + guard let query2 = value.query2 else { + let message = "Creating a URL Query Item failed. query2 is required and must not be nil." + throw ClientRuntime.ClientError.unknownError(message) + } + query2.forEach { queryItemValue in + let queryItem = ClientRuntime.SDKURLQueryItem(name: "Query2".urlPercentEncoding(), value: Swift.String(TimestampFormatter(format: .dateTime).string(from: queryItemValue)).urlPercentEncoding()) + items.append(queryItem) + } + guard let query3 = value.query3 else { + let message = "Creating a URL Query Item failed. query3 is required and must not be nil." + throw ClientRuntime.ClientError.unknownError(message) + } + let query3QueryItem = ClientRuntime.SDKURLQueryItem(name: "Query3".urlPercentEncoding(), value: Swift.String(query3).urlPercentEncoding()) + items.append(query3QueryItem) + return items + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } diff --git a/smithy-swift-codegen/src/test/kotlin/HttpUrlPathProviderTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpUrlPathProviderTests.kt index 646dba89d..e85ed3a55 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpUrlPathProviderTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpUrlPathProviderTests.kt @@ -8,17 +8,17 @@ class HttpUrlPathProviderTests { val context = setupTests("http-binding-protocol-generator-test.smithy") val contents = getModelFileContents("example", "SmokeTestInput+UrlPathProvider.swift", context.manifest) contents.shouldSyntacticSanityCheck() - val expectedContents = - """ - extension SmokeTestInput: ClientRuntime.URLPathProvider { - public var urlPath: Swift.String? { - guard let label1 = label1 else { - return nil - } - return "/smoketest/\(label1.urlPercentEncoding())/foo" - } - } - """.trimIndent() + val expectedContents = """ +extension SmokeTestInput { + + static func urlPathProvider(_ value: SmokeTestInput) -> Swift.String? { + guard let label1 = value.label1 else { + return nil + } + return "/smoketest/\(label1.urlPercentEncoding())/foo" + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } @@ -29,20 +29,20 @@ class HttpUrlPathProviderTests { contents.shouldSyntacticSanityCheck() // All http labels are implicitly required, even if the smithy spec doesn't specify the required trait - val expectedContents = - """ - extension RequiredHttpFieldsInput: ClientRuntime.URLPathProvider { - public var urlPath: Swift.String? { - guard let label1 = label1 else { - return nil - } - guard let label2 = label2 else { - return nil - } - return "/RequiredHttpFields/\(label1.urlPercentEncoding())/\(label2.urlPercentEncoding())" - } - } - """.trimIndent() + val expectedContents = """ +extension RequiredHttpFieldsInput { + + static func urlPathProvider(_ value: RequiredHttpFieldsInput) -> Swift.String? { + guard let label1 = value.label1 else { + return nil + } + guard let label2 = value.label2 else { + return nil + } + return "/RequiredHttpFields/\(label1.urlPercentEncoding())/\(label2.urlPercentEncoding())" + } +} +""" contents.shouldContainOnlyOnce(expectedContents) } diff --git a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt index 65fd24acc..cefdb1599 100644 --- a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt @@ -1,6 +1,5 @@ import io.kotest.matchers.string.shouldContainOnlyOnce import org.junit.jupiter.api.Test - class IdempotencyTokenTraitTests { @Test fun `generates idempotent middleware`() { @@ -24,7 +23,7 @@ class IdempotencyTokenTraitTests { .build() var operation = ClientRuntime.OperationStack(id: "idempotencyTokenWithStructure") operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.IdempotencyTokenMiddleware(keyPath: \.token)) - operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware()) + operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLPathMiddleware(IdempotencyTokenWithStructureInput.urlPathProvider(_:))) operation.initializeStep.intercept(position: .after, middleware: ClientRuntime.URLHostMiddleware()) operation.buildStep.intercept(position: .before, middleware: ClientRuntime.AuthSchemeMiddleware()) operation.serializeStep.intercept(position: .after, middleware: ContentTypeMiddleware(contentType: "application/xml"))