From bb7045de72de39c853415d0f8b37aa4687e58b21 Mon Sep 17 00:00:00 2001 From: Chan Yoo <55515281+sichanyoo@users.noreply.github.com> Date: Fri, 9 Feb 2024 14:34:07 -0800 Subject: [PATCH] chore: Merge latest changes from main into SRA I&A (#662) * chore: Require Swift 5.7, fix deprecation warnings (#600) * feat: support initial-response in RPC based event streams (#597) * chore: Updates version to 0.32.0 * chore: Add newline to README.md (#602) * feat: add limited support in smithy-swift for visionOS (#606) * feat: add support for requiresLength trait and Transfer-Encoding: Chunked (#604) * chore: Update to aws-crt-swift 0.15.0 (#607) * fix: content-length middleware should not error on event streams (#608) * chore: Updates version to 0.33.0 * chore: Improved downstream task (#568) * chore: Convert idempotency token middleware from closure to reusable type (#610) * fix: Update aws-crt-swift dependency to 0.17.0 (#612) * chore: Updates version to 0.34.0 * fix: Endpoint url should be nil if host or scheme is missing (#614) * fix: Pool HTTP connections based on scheme, host, and port (#615) * add default log level to initialize method (#616) * feat: add utility method for converting SdkHttpRequest to URLRequest. (#613) * Add extension constructor to URLRequest to convert SDKHttpRequest * Add preprocessor conditional import functionality to SwiftWriter. --------- Co-authored-by: Sichan Yoo * chore: Updates version to 0.35.0 * fix: Add a header to operation doc comments (#621) * remove unnecessary TODOs (#622) * fix: Codegen issues re: recursion, Swift keywords in unions (#623) * feat!: Replace the XML encoder with a custom Smithy implementation (#619) * feat!: Use closures for processing HTTP response (#624) * feat: add custom trait PaginationTruncationMember (#625) * allow isTruncated to be optional bool (#626) * chore: Updates version to 0.36.0 * chore: Run tvOS old & new in CI (#628) * fix: Fix Package.swift warning on Mac (#629) * chore: refactor HttpBody and ByteStream to be a single class ByteStream (#627) * chore: remove sync read in unused data extension (#630) * update smithy to 1.42.0 (#631) * chore: Updates version to 0.37.0 * chore: Update to aws-crt-swift 0.20.0 (#633) * fix: add back from method with fileHandle (#635) * fix!: Add no-op behavior for initialize methods of logging system. (#637) * Add no-op behavior for initialize methods if it isn't the first time being called. * Make LockingSystem threadsafe. * Make initialize methods async. --------- Co-authored-by: Sichan Yoo * feat!: URLSession-based HTTP Client (#636) * bump up CRT version to 0.22.0 (#639) * chore: Update version to 0.38.0 (#641) * chore: Empty commit (#643) * feat: add wrapper for checksums + unit tests (#642) * feat: Use the Foundation HTTP client by default on Mac (#646) * chore: Updates version to 0.39.0 * chore: Change MyURLQueryItem to SDKURLQueryItem (#652) * feat!: Provide HTTP request components by closure instead of protocol (#654) * fix: Don't retry modeled errors by default (#653) * feat!: Eliminate service client protocols (#655) * feat: add support for flexible checksums on Normal payloads (#647) * chore: Update aws-crt-swift to 0.26.0 (#661) --------- Co-authored-by: Josh Elkins Co-authored-by: David Yaffe Co-authored-by: AWS SDK Swift Automation Co-authored-by: Cyprien Ricque <48893621+CyprienRicque@users.noreply.github.com> Co-authored-by: Sichan Yoo --- .../Networking/HashFunction.swift | 75 +++++++- .../Middlewares/ContentMD5Middleware.swift | 5 + .../FlexibleChecksumsRequestMiddleware.swift | 87 +++++++++ .../FlexibleChecksumsResponseMiddleware.swift | 115 +++++++++++ ...xibleChecksumsRequestMiddlewareTests.swift | 182 ++++++++++++++++++ .../swift/codegen/ClientRuntimeTypes.kt | 2 + .../smithy/swift/codegen/CodegenVisitor.kt | 5 +- .../smithy/swift/codegen/ServiceGenerator.kt | 84 +------- .../codegen/ServiceNamespaceGenerator.kt | 23 --- .../codegen/ServiceNamespaceIntegration.kt | 26 +++ .../HttpProtocolClientGenerator.kt | 15 +- .../middlewares/ContentMD5Middleware.kt | 12 +- .../MiddlewareExecutionGenerator.kt | 9 +- .../swift/codegen/waiters/WaiterGenerator.kt | 4 +- ...swift.codegen.integration.SwiftIntegration | 3 +- .../test/kotlin/ContentMd5MiddlewareTests.kt | 3 +- .../HttpProtocolClientGeneratorTests.kt | 17 +- .../test/kotlin/IdempotencyTokenTraitTests.kt | 3 +- .../src/test/kotlin/ServiceGeneratorTests.kt | 122 ------------ .../kotlin/waiters/WaiterGeneratorTests.kt | 4 +- 20 files changed, 538 insertions(+), 258 deletions(-) create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/FlexibleChecksumsRequestMiddleware.swift create mode 100644 Sources/ClientRuntime/Networking/Http/Middlewares/FlexibleChecksumsResponseMiddleware.swift create mode 100644 Tests/ClientRuntimeTests/NetworkingTests/FlexibleChecksumsRequestMiddlewareTests.swift delete mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceNamespaceGenerator.kt create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceNamespaceIntegration.kt delete mode 100644 smithy-swift-codegen/src/test/kotlin/ServiceGeneratorTests.kt diff --git a/Sources/ClientRuntime/Networking/HashFunction.swift b/Sources/ClientRuntime/Networking/HashFunction.swift index 73129da48..161eab86b 100644 --- a/Sources/ClientRuntime/Networking/HashFunction.swift +++ b/Sources/ClientRuntime/Networking/HashFunction.swift @@ -5,17 +5,17 @@ import AwsCommonRuntimeKit -enum HashResult { +public enum HashResult { case data(Data) case integer(UInt32) } -enum HashError: Error { +public enum HashError: Error { case invalidInput case hashingFailed(reason: String) } -enum HashFunction { +public enum HashFunction { case crc32, crc32c, sha1, sha256, md5 static func from(string: String) -> HashFunction? { @@ -29,7 +29,29 @@ enum HashFunction { } } - var isSupported: Bool { + static func fromList(_ stringArray: [String]) -> [HashFunction] { + var hashFunctions = [HashFunction]() + + for string in stringArray { + if let hashFunction = HashFunction.from(string: string) { + hashFunctions.append(hashFunction) + } + } + + return hashFunctions + } + + func toString() -> String { + switch self { + case .crc32: return "crc32" + case .crc32c: return "crc32c" + case .sha1: return "sha1" + case .sha256: return "sha256" + case .md5: return "md5" + } + } + + var isFlexibleChecksum: Bool { switch self { case .crc32, .crc32c, .sha256, .sha1: return true @@ -69,6 +91,41 @@ enum HashFunction { } } +extension HashFunction: Comparable { + /* + * Priority-order for validating checksum = [ CRC32C, CRC32, SHA1, SHA256 ] + * Order is determined by speed of the algorithm's implementation + * MD5 is not supported by list ordering + */ + public static func < (lhs: HashFunction, rhs: HashFunction) -> Bool { + let order: [HashFunction] = [.crc32c, .crc32, .sha1, .sha256] + + let lhsIndex = order.firstIndex(of: lhs) ?? Int.max + let rhsIndex = order.firstIndex(of: rhs) ?? Int.max + + return lhsIndex < rhsIndex + } +} + +extension [HashFunction] { + func getPriorityOrderValidationList() -> [HashFunction] { + // Filter out .md5 if present and then sort the remaining hash functions + return self.filter { $0 != .md5 }.sorted() + } +} + +extension UInt32 { + func toBase64EncodedString() -> String { + // Create a Data instance from the UInt32 value + var value = self + var bigEndianValue = value.bigEndian + var data = Data(bytes: &bigEndianValue, count: MemoryLayout.size) + + // Base64 encode the data + return data.base64EncodedString() + } +} + extension HashResult { // Convert a HashResult to a hexadecimal String @@ -80,4 +137,14 @@ extension HashResult { return String(format: "%08x", integer) } } + + // Convert a HashResult to a base64-encoded String + func toBase64String() -> String { + switch self { + case .data(let data): + return data.base64EncodedString() + case .integer(let integer): + return integer.toBase64EncodedString() + } + } } diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift index 766d551c3..dad7107e4 100644 --- a/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/ContentMD5Middleware.swift @@ -18,6 +18,11 @@ public struct ContentMD5Middleware: Middleware { Self.MOutput == H.Output, Self.Context == H.Context { + // Skip MD5 hash if using checksums + if (input.headers.exists(name: "x-amz-sdk-checksum-algorithm")) { + return try await next.handle(context: context, input: input) + } + switch input.body { case .data(let data): guard let data else { diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/FlexibleChecksumsRequestMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/FlexibleChecksumsRequestMiddleware.swift new file mode 100644 index 000000000..9b037a9c7 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/FlexibleChecksumsRequestMiddleware.swift @@ -0,0 +1,87 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +public struct FlexibleChecksumsRequestMiddleware: Middleware { + + public let id: String = "FlexibleChecksumsRequestMiddleware" + + let checksumAlgorithm: String? + + public init(checksumAlgorithm: String?) { + self.checksumAlgorithm = checksumAlgorithm + } + + public func handle(context: Context, + input: SerializeStepInput, + next: H) async throws -> OperationOutput + where H: Handler, + Self.MInput == H.Input, + Self.MOutput == H.Output, + Self.Context == H.Context { + + // Initialize logger + guard let logger = context.getLogger() else { + throw ClientError.unknownError("No logger found!") + } + + guard let checksumString = checksumAlgorithm else { + logger.info("No checksum provided! Skipping flexible checksums workflow...") + return try await next.handle(context: context, input: input) + } + + guard let checksumHashFunction = HashFunction.from(string: checksumString) else { + logger.info("Found no supported checksums! Skipping flexible checksums workflow...") + return try await next.handle(context: context, input: input) + } + + // Determine the header name + let headerName = "x-amz-checksum-\(checksumHashFunction)" + logger.debug("Resolved checksum header name: \(headerName)") + + // Get the request + let request = input.builder + + func handleNormalPayload(_ data: Data?) throws { + + // Check if any checksum header is already provided by the user + let checksumHeaderPrefix = "x-amz-checksum-" + if request.headers.headers.contains(where: { $0.name.lowercased().starts(with: checksumHeaderPrefix) }) { + logger.debug("Checksum header already provided by the user. Skipping calculation.") + return + } + + guard let data else { + throw ClientError.dataNotFound("Cannot calculate checksum of empty body!") + } + + if input.builder.headers.value(for: headerName) == nil { + logger.debug("Calculating checksum") + } + + let checksum = try checksumHashFunction.computeHash(of: data).toBase64String() + + request.updateHeader(name: headerName, value: [checksum]) + } + + func handleStreamPayload(_ stream: Stream) throws { + logger.error("Stream payloads are not yet supported with flexible checksums!") + return + } + + // Handle body vs handle stream + switch request.body { + case .data(let data): + try handleNormalPayload(data) + case .stream(let stream): + try handleStreamPayload(stream) + case .noStream: + throw ClientError.dataNotFound("Cannot calculate the checksum of an empty body!") + } + + return try await next.handle(context: context, input: input) + } + + public typealias MInput = SerializeStepInput + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} diff --git a/Sources/ClientRuntime/Networking/Http/Middlewares/FlexibleChecksumsResponseMiddleware.swift b/Sources/ClientRuntime/Networking/Http/Middlewares/FlexibleChecksumsResponseMiddleware.swift new file mode 100644 index 000000000..366b39807 --- /dev/null +++ b/Sources/ClientRuntime/Networking/Http/Middlewares/FlexibleChecksumsResponseMiddleware.swift @@ -0,0 +1,115 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +public struct FlexibleChecksumsResponseMiddleware: Middleware { + + public let id: String = "FlexibleChecksumsResponseMiddleware" + + // The priority to validate response checksums, if multiple are present + let CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST: [String] = [ + HashFunction.crc32c, + .crc32, + .sha1, + .sha256 + ].map { $0.toString() } + + let validationMode: Bool + + public init(validationMode: Bool) { + self.validationMode = validationMode + } + + public func handle(context: Context, + input: SdkHttpRequest, + next: H) async throws -> OperationOutput + where H: Handler, + Self.MInput == H.Input, + Self.MOutput == H.Output, + Self.Context == H.Context { + + // The name of the checksum header which was validated. If `null`, validation was not performed. + context.attributes.set(key: AttributeKey(name: "ChecksumHeaderValidated"), value: nil) + + // Initialize logger + guard let logger = context.getLogger() else { + throw ClientError.unknownError("No logger found!") + } + + // Exit if validation should not be performed + if !validationMode { + logger.info("Checksum validation should not be performed! Skipping workflow...") + return try await next.handle(context: context, input: input) + } + + // Get the response + let response = try await next.handle(context: context, input: input) + let httpResponse = response.httpResponse + + // Determine if any checksum headers are present + logger.debug("HEADERS: \(httpResponse.headers)") + let _checksumHeader = CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST.first { + httpResponse.headers.value(for: "x-amz-checksum-\($0)") != nil + } + guard let checksumHeader = _checksumHeader else { + logger.warn( + "User requested checksum validation, but the response headers did not contain any valid checksums" + ) + return try await next.handle(context: context, input: input) + } + + let fullChecksumHeader = "x-amz-checksum-" + checksumHeader + + // let the user know which checksum will be validated + logger.debug("Validating checksum from \(fullChecksumHeader)") + context.attributes.set(key: AttributeKey(name: "ChecksumHeaderValidated"), value: fullChecksumHeader) + + let checksumString = checksumHeader.removePrefix("x-amz-checksum-") + guard let responseChecksum = HashFunction.from(string: checksumString) else { + throw ClientError.dataNotFound("Checksum found in header is not supported!") + } + guard let expectedChecksum = httpResponse.headers.value(for: fullChecksumHeader) else { + throw ClientError.dataNotFound("Could not determine the expected checksum!") + } + + func handleNormalPayload(_ data: Data?) throws { + + guard let data else { + throw ClientError.dataNotFound("Cannot calculate checksum of empty body!") + } + + let calculatedChecksum = try responseChecksum.computeHash(of: data) + + let actualChecksum = calculatedChecksum.toBase64String() + + if expectedChecksum != actualChecksum { + throw ChecksumMismatchException.message( + "Checksum mismatch. Expected \(expectedChecksum) but was \(actualChecksum)" + ) + } + } + + func handleStreamPayload(_ stream: Stream) throws { + return + } + + // Handle body vs handle stream + switch response.httpResponse.body { + case .data(let data): + try handleNormalPayload(data) + case .stream(let stream): + try handleStreamPayload(stream) + case .noStream: + throw ClientError.dataNotFound("Cannot calculate the checksum of an empty body!") + } + + return try await next.handle(context: context, input: input) + } + + public typealias MInput = SdkHttpRequest + public typealias MOutput = OperationOutput + public typealias Context = HttpContext +} + +enum ChecksumMismatchException: Error { + case message(String) +} diff --git a/Tests/ClientRuntimeTests/NetworkingTests/FlexibleChecksumsRequestMiddlewareTests.swift b/Tests/ClientRuntimeTests/NetworkingTests/FlexibleChecksumsRequestMiddlewareTests.swift new file mode 100644 index 000000000..3a2d78c98 --- /dev/null +++ b/Tests/ClientRuntimeTests/NetworkingTests/FlexibleChecksumsRequestMiddlewareTests.swift @@ -0,0 +1,182 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0. + +import XCTest +import AwsCommonRuntimeKit +import SmithyTestUtil +@testable import ClientRuntime + +class FlexibleChecksumsRequestMiddlewareTests: XCTestCase { + private var builtContext: HttpContext! + private var stack: OperationStack! + private let testLogger = TestLogger() + + override func setUpWithError() throws { + try super.setUpWithError() + + // Initialize function needs to be called before interacting with CRT + CommonRuntimeKit.initialize() + + builtContext = HttpContextBuilder() + .withMethod(value: .get) + .withPath(value: "/") + .withEncoder(value: JSONEncoder()) + .withDecoder(value: JSONDecoder()) + .withOperation(value: "Test Operation") + .withLogger(value: testLogger) + .build() + stack = OperationStack(id: "Test Operation") + } + + func testNormalPayloadSha256() async throws { + let checksumAlgorithm = "sha256" + let testData = ByteStream.data(Data("Hello, world!".utf8)) + setNormalPayload( + payload: testData + ) + addFlexibleChecksumsRequestMiddleware(checksumAlgorithm: checksumAlgorithm) + addFlexibleChecksumsResponseMiddleware(validationMode: true) + try await AssertHeaderIsPresentAndValidationOccurs( + expectedHeader: "x-amz-checksum-sha256", + responseBody: testData, + expectedChecksum: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=" + ) + } + + func testNormalPayloadSha1() async throws { + let checksumAlgorithm = "sha1" + let testData = ByteStream.data(Data("Hello, world!".utf8)) + setNormalPayload( + payload: testData + ) + addFlexibleChecksumsRequestMiddleware(checksumAlgorithm: checksumAlgorithm) + addFlexibleChecksumsResponseMiddleware(validationMode: true) + try await AssertHeaderIsPresentAndValidationOccurs( + expectedHeader: "x-amz-checksum-sha1", + responseBody: testData, + expectedChecksum: "lDpwLQbzRZmu4fjajvn3KWAx1pk=" + ) + } + + func testNormalPayloadCRC32() async throws { + let checksumAlgorithm = "crc32" + let testData = ByteStream.data(Data("Hello, world!".utf8)) + setNormalPayload( + payload: testData + ) + addFlexibleChecksumsRequestMiddleware(checksumAlgorithm: checksumAlgorithm) + addFlexibleChecksumsResponseMiddleware(validationMode: true) + try await AssertHeaderIsPresentAndValidationOccurs( + expectedHeader: "x-amz-checksum-crc32", + responseBody: testData, + expectedChecksum: "6+bG5g==" + ) + } + + func testNormalPayloadCRC32C() async throws { + let checksumAlgorithm = "crc32c" + let testData = ByteStream.data(Data("Hello, world!".utf8)) + setNormalPayload( + payload: testData + ) + addFlexibleChecksumsRequestMiddleware(checksumAlgorithm: checksumAlgorithm) + addFlexibleChecksumsResponseMiddleware(validationMode: true) + try await AssertHeaderIsPresentAndValidationOccurs( + expectedHeader: "x-amz-checksum-crc32c", + responseBody: testData, + expectedChecksum: "yKEG5Q==" + ) + } + + func testNilChecksumAlgorithm() async throws { + let testData = ByteStream.data(Data("Hello, world!".utf8)) + setNormalPayload( + payload: testData + ) + addFlexibleChecksumsRequestMiddleware(checksumAlgorithm: nil) + addFlexibleChecksumsResponseMiddleware(validationMode: false) + try await AssertHeaderIsPresentAndValidationOccurs( + checkLogs: [ + "No checksum provided! Skipping flexible checksums workflow...", + "Checksum validation should not be performed! Skipping workflow..." + ] + ) + } + + private func addFlexibleChecksumsRequestMiddleware(checksumAlgorithm: String?) { + stack.serializeStep.intercept( + position: .after, + middleware: FlexibleChecksumsRequestMiddleware(checksumAlgorithm: checksumAlgorithm) + ) + } + + private func addFlexibleChecksumsResponseMiddleware(validationMode: Bool) { + stack.deserializeStep.intercept( + position: .after, + middleware: FlexibleChecksumsResponseMiddleware(validationMode: validationMode) + ) + } + + private func setNormalPayload(payload: ByteStream) { + // Set normal payload data + stack.serializeStep.intercept(position: .before, id: "set normal payload") { (context, input, next) -> OperationOutput in + input.builder.body = payload // Set the payload data here + return try await next.handle(context: context, input: input) + } + } + + private func AssertHeaderIsPresentAndValidationOccurs( + expectedHeader: String = "", + responseBody: ByteStream = ByteStream.noStream, + expectedChecksum: String = "", + checkLogs: [String] = [] + ) async throws -> Void { + let file: StaticString = #file + let line: UInt = #line + var isChecksumValidated = false + let mockHandler = MockHandler { (_, input) in + if expectedHeader != "" { + XCTAssert(input.headers.value(for: expectedHeader) != nil, file: file, line: line) + } + let httpResponse = HttpResponse(body: responseBody, statusCode: HttpStatusCode.ok) + httpResponse.headers.add(name: expectedHeader, value: expectedChecksum) + let mockOutput = try! MockOutput(httpResponse: httpResponse, decoder: nil) + let output = OperationOutput(httpResponse: httpResponse, output: mockOutput) + if let validatedChecksum = self.builtContext.attributes.get(key: AttributeKey(name: "ChecksumHeaderValidated")), validatedChecksum == expectedHeader { + isChecksumValidated = true + } + return output + } + + _ = try await stack.handleMiddleware(context: builtContext, input: MockInput(), next: mockHandler) + + if !checkLogs.isEmpty { + checkLogs.forEach { expectedLog in + XCTAssertTrue(testLogger.messages.contains { $0.level == .info && $0.message.contains(expectedLog) }, "Expected log message not found") + } + } + + if expectedChecksum != "" { + // Assert that the expected checksum was validated + XCTAssertTrue(isChecksumValidated, "Checksum was not validated as expected.") + } + } +} + +class TestLogger: LogAgent { + var name: String + + var messages: [(level: LogAgentLevel, message: String)] = [] + + var level: ClientRuntime.LogAgentLevel + + init(name: String = "Test", messages: [(level: LogAgentLevel, message: String)] = [], level: ClientRuntime.LogAgentLevel = .info) { + self.name = name + self.messages = messages + self.level = level + } + + func log(level: ClientRuntime.LogAgentLevel = .info, message: String, metadata: [String : String]? = nil, source: String = "ChecksumUnitTests", file: String = #file, function: String = #function, line: UInt = #line) { + messages.append((level: level, message: message)) + } +} 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 e1fc8a69c..03df29837 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 @@ -66,6 +66,8 @@ object ClientRuntimeTypes { val LoggerMiddleware = runtimeSymbol("LoggerMiddleware") val ContentLengthMiddleware = runtimeSymbol("ContentLengthMiddleware") val ContentMD5Middleware = runtimeSymbol("ContentMD5Middleware") + val FlexibleChecksumsRequestMiddleware = runtimeSymbol("FlexibleChecksumsRequestMiddleware") + val FlexibleChecksumsResponseMiddleware = runtimeSymbol("FlexibleChecksumsResponseMiddleware") val DeserializeMiddleware = runtimeSymbol("DeserializeMiddleware") val MutateHeadersMiddleware = runtimeSymbol("MutateHeadersMiddleware") val OperationStack = runtimeSymbol("OperationStack") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt index ed1c3b00d..3b5e0fd55 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/CodegenVisitor.kt @@ -185,10 +185,7 @@ class CodegenVisitor(context: PluginContext) : ShapeVisitor.Default() { } override fun serviceShape(shape: ServiceShape): Void? { - writers.useShapeWriter(shape) { writer: SwiftWriter -> - ServiceGenerator(settings, model, symbolProvider, writer, writers, protocolGenerator, protocolContext).render() - ServiceNamespaceGenerator(settings, model, symbolProvider, writer).render() - } + // This used to generate the client protocol. No longer used. return null } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceGenerator.kt index d63c59531..6aa39bd5f 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceGenerator.kt @@ -6,40 +6,22 @@ package software.amazon.smithy.swift.codegen import software.amazon.smithy.codegen.core.CodegenException -import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolProvider import software.amazon.smithy.model.Model import software.amazon.smithy.model.knowledge.OperationIndex -import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.ShapeId import software.amazon.smithy.model.shapes.StructureShape import software.amazon.smithy.model.traits.DocumentationTrait import software.amazon.smithy.model.traits.StreamingTrait -import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.model.toLowerCamelCase -/* -* Generates a Swift protocol for the service - */ -class ServiceGenerator( - settings: SwiftSettings, - private val model: Model, - private val symbolProvider: SymbolProvider, - private val writer: SwiftWriter, - private val delegator: SwiftDelegator, - private val protocolGenerator: ProtocolGenerator? = null, - private val protocolGenerationContext: ProtocolGenerator.GenerationContext? -) { - private var service = settings.getService(model) - private val serviceSymbol: Symbol by lazy { - symbolProvider.toSymbol(service) - } +class ServiceGenerator() { companion object { /** - * Renders the definition of operation + * Renders the definition of operation, followed by no CR */ fun renderOperationDefinition( model: Model, @@ -64,11 +46,12 @@ class ServiceGenerator( val accessSpecifier = if (insideProtocol) "" else "public " - writer.write( - "${accessSpecifier}func \$L(\$L) async throws -> \$L", + writer.writeInline( + "\$Lfunc \$L(\$L) async throws -> \$L", + accessSpecifier, operationName, inputParam, - outputShapeName + outputShapeName, ) } @@ -110,61 +93,6 @@ class ServiceGenerator( return docTrait?.value ?: "[no documentation found]" } } - - fun render() { - // add imports - writer.addImport(serviceSymbol) - - // generate protocol - renderSwiftProtocol() - } - - /** - * Generates an appropriate Swift Protocol for a Smithy Service shape. - * - * For example, given the following Smithy model: - * - * ``` - * namespace smithy.example - * - * use aws.protocols#awsJson1_1 - * - * @awsJson1_1 - * service Example { - * version: "1.0.0", - * operations: [GetFoo] - * } - * - * operation GetFoo { - * input: GetFooInput, - * output: GetFooOutput, - * errors: [GetFooError] - * } - * - * ``` - * We will generate the following: - * ``` - * public protocol ExampleServiceProtocol { - * func getFoo(input: GetFooInput) async throws -> GetFooResponse - * } - * ``` - */ - private fun renderSwiftProtocol() { - val topDownIndex = TopDownIndex.of(model) - val operations = topDownIndex.getContainedOperations(service) - val operationsIndex = OperationIndex.of(model) - - writer.writeShapeDocs(service) - writer.writeAvailableAttribute(model, service) - writer.openBlock("public protocol ${serviceSymbol.name}Protocol {") - .call { - operations.forEach { op -> - renderOperationDefinition(model, service, symbolProvider, writer, operationsIndex, op, true) - } - } - .closeBlock("}") - .write("") - } } /** diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceNamespaceGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceNamespaceGenerator.kt deleted file mode 100644 index 53b7d477b..000000000 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceNamespaceGenerator.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -package software.amazon.smithy.swift.codegen - -import software.amazon.smithy.codegen.core.SymbolProvider -import software.amazon.smithy.model.Model -import software.amazon.smithy.swift.codegen.model.nestedNamespaceType - -class ServiceNamespaceGenerator( - private val settings: SwiftSettings, - private val model: Model, - private val symbolProvider: SymbolProvider, - private val writer: SwiftWriter, -) { - - fun render() { - val service = settings.getService(model) - writer.write("public enum ${service.nestedNamespaceType(symbolProvider)} {}") - } -} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceNamespaceIntegration.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceNamespaceIntegration.kt new file mode 100644 index 000000000..59b0c7ea3 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/ServiceNamespaceIntegration.kt @@ -0,0 +1,26 @@ +package software.amazon.smithy.swift.codegen + +import software.amazon.smithy.model.Model +import software.amazon.smithy.swift.codegen.core.CodegenContext +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.SwiftIntegration +import software.amazon.smithy.swift.codegen.model.nestedNamespaceType + +class ServiceNamespaceIntegration : SwiftIntegration { + override fun enabledForService(model: Model, settings: SwiftSettings): Boolean { + return true + } + + override fun writeAdditionalFiles( + ctx: CodegenContext, + protoCtx: ProtocolGenerator.GenerationContext, + delegator: SwiftDelegator + ) { + val service = ctx.settings.getService(ctx.model) + val namespaceName = service.nestedNamespaceType(ctx.symbolProvider) + val filename = "${ctx.settings.moduleName}/Models/$namespaceName.swift" + delegator.useFileWriter(filename) { writer -> + writer.write("public enum \$L {}", namespaceName) + } + } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt index 8f0d9b311..930dce901 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/HttpProtocolClientGenerator.kt @@ -48,16 +48,23 @@ open class HttpProtocolClientGenerator( val operations = topDownIndex.getContainedOperations(serviceShape).sortedBy { it.toUpperCamelCase() } val operationsIndex = OperationIndex.of(model) - writer.openBlock("extension ${serviceSymbol.name}: ${serviceSymbol.name}Protocol {", "}") { + writer.openBlock("extension \$L {", "}", serviceSymbol.name) { operations.forEach { ServiceGenerator.renderOperationDefinition(model, serviceShape, symbolProvider, writer, operationsIndex, it) - writer.openBlock("{", "}") { + writer.openBlock(" {", "}") { val operationStackName = "operation" val generator = MiddlewareExecutionGenerator(ctx, writer, httpBindingResolver, httpProtocolCustomizable, operationMiddleware, operationStackName) generator.render(serviceShape, it) { writer, labelMemberName -> - writer.write("throw \$N(\"uri component $labelMemberName unexpectedly nil\")", ClientRuntimeTypes.Core.UnknownClientError) + writer.write( + "throw \$N(\"uri component \$L unexpectedly nil\")", + ClientRuntimeTypes.Core.UnknownClientError, + labelMemberName, + ) } - writer.write("let result = try await $operationStackName.handleMiddleware(context: context, input: input, next: client.getHandler())") + writer.write( + "let result = try await \$L.handleMiddleware(context: context, input: input, next: client.getHandler())", + operationStackName, + ) writer.write("return result") } writer.write("") diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentMD5Middleware.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentMD5Middleware.kt index 68c1ce7dd..8031d4f54 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentMD5Middleware.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/middlewares/ContentMD5Middleware.kt @@ -17,7 +17,7 @@ import software.amazon.smithy.swift.codegen.model.hasTrait class ContentMD5Middleware( val model: Model, - val symbolProvider: SymbolProvider + val symbolProvider: SymbolProvider, ) : MiddlewareRenderable { override val name = "ContentMD5Middleware" @@ -26,13 +26,17 @@ class ContentMD5Middleware( override val position = MiddlewarePosition.BEFORE override fun render(ctx: ProtocolGenerator.GenerationContext, writer: SwiftWriter, op: OperationShape, operationStackName: String) { - if (op.isChecksumRequired()) { + if (op.isMD5ChecksumRequired()) { val outputShapeName = MiddlewareShapeUtils.outputSymbol(symbolProvider, model, op).name writer.write("$operationStackName.${middlewareStep.stringValue()}.intercept(position: ${position.stringValue()}, middleware: \$N<$outputShapeName>())", ClientRuntimeTypes.Middleware.ContentMD5Middleware) } } } -// TODO https://github.com/awslabs/aws-sdk-swift/issues/653 -private fun OperationShape.isChecksumRequired(): Boolean = +/** + * Check if MD5 checksum is required + * The Md5 middleware will only be installed if the operation requires a checksum and the user has not opted-in to flexible checksums. + */ +private fun OperationShape.isMD5ChecksumRequired(): Boolean = + // the checksum requirement can be modeled in either HttpChecksumTrait's `requestChecksumRequired` or the HttpChecksumRequired trait getTrait()?.isRequestChecksumRequired == true || hasTrait() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt index c5a254d33..edf6081f6 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt @@ -42,7 +42,14 @@ class MiddlewareExecutionGenerator( renderContextAttributes(op, flowType) } httpProtocolCustomizable.renderEventStreamAttributes(ctx, writer, op) - writer.write("var $operationStackName = \$N<$inputShapeName, $outputShapeName>(id: \"${op.toLowerCamelCase()}\")", OperationStack) + writer.write( + "var \$L = \$N<\$L, \$L>(id: \$S)", + operationStackName, + OperationStack, + inputShapeName, + outputShapeName, + op.toLowerCamelCase(), + ) renderMiddlewares(ctx, op, operationStackName) } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/waiters/WaiterGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/waiters/WaiterGenerator.kt index e58727934..94e189aa0 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/waiters/WaiterGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/waiters/WaiterGenerator.kt @@ -33,8 +33,8 @@ class WaiterGenerator( writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) val serviceSymbol = ctx.symbolProvider.toSymbol(service) - // Render an extension on the service protocol, which will contain the waitUntil... methods - writer.openBlock("extension \$LProtocol {", "}", serviceSymbol.name) { + // Render an extension on the service client, which will hold the waitUntil... methods + writer.openBlock("extension \$L {", "}", serviceSymbol.name) { // Get the operation shapes for this service val operationShapes = service.allOperations diff --git a/smithy-swift-codegen/src/main/resources/META-INF/services/software.amazon.smithy.swift.codegen.integration.SwiftIntegration b/smithy-swift-codegen/src/main/resources/META-INF/services/software.amazon.smithy.swift.codegen.integration.SwiftIntegration index 7ae506cf8..cfd77c32c 100644 --- a/smithy-swift-codegen/src/main/resources/META-INF/services/software.amazon.smithy.swift.codegen.integration.SwiftIntegration +++ b/smithy-swift-codegen/src/main/resources/META-INF/services/software.amazon.smithy.swift.codegen.integration.SwiftIntegration @@ -1,2 +1,3 @@ software.amazon.smithy.swift.codegen.PaginatorGenerator -software.amazon.smithy.swift.codegen.waiters.WaiterIntegration \ No newline at end of file +software.amazon.smithy.swift.codegen.waiters.WaiterIntegration +software.amazon.smithy.swift.codegen.ServiceNamespaceIntegration \ No newline at end of file diff --git a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt index e6ed992de..8e901ed7a 100644 --- a/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/ContentMd5MiddlewareTests.kt @@ -7,8 +7,7 @@ class ContentMd5MiddlewareTests { val context = setupTests("Isolated/contentmd5checksum.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/RestXmlProtocolClient.swift") val expectedContents = """ - public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput - { + public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput { let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) diff --git a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt index 03d2b40e0..243ae92f8 100644 --- a/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/HttpProtocolClientGeneratorTests.kt @@ -94,7 +94,7 @@ class HttpProtocolClientGeneratorTests { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() - contents.shouldContainOnlyOnce("extension RestJsonProtocolClient: RestJsonProtocolClientProtocol {") + contents.shouldContainOnlyOnce("extension RestJsonProtocolClient {") } @Test fun `it renders an operation body`() { @@ -102,8 +102,7 @@ class HttpProtocolClientGeneratorTests { val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() val expected = """ - public func allocateWidget(input: AllocateWidgetInput) async throws -> AllocateWidgetOutput - { + public func allocateWidget(input: AllocateWidgetInput) async throws -> AllocateWidgetOutput { let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -135,14 +134,14 @@ class HttpProtocolClientGeneratorTests { """ contents.shouldContainOnlyOnce(expected) } + @Test fun `ContentLengthMiddleware generates correctly with requiresLength false and unsignedPayload true`() { val context = setupTests("service-generator-test-operations.smithy", "com.test#Example") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() val expected = """ - public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput - { + public func unsignedFooBlobStream(input: UnsignedFooBlobStreamInput) async throws -> UnsignedFooBlobStreamOutput { let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -173,14 +172,14 @@ 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") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() val expected = """ - public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput - { + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput { let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) @@ -211,14 +210,14 @@ 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") val contents = getFileContents(context.manifest, "/RestJson/RestJsonProtocolClient.swift") contents.shouldSyntacticSanityCheck() val expected = """ - public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput - { + public func unsignedFooBlobStreamWithLength(input: UnsignedFooBlobStreamWithLengthInput) async throws -> UnsignedFooBlobStreamWithLengthOutput { let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) diff --git a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt index cefdb1599..f2366fb4e 100644 --- a/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/IdempotencyTokenTraitTests.kt @@ -6,8 +6,7 @@ class IdempotencyTokenTraitTests { val context = setupTests("Isolated/idempotencyToken.smithy", "aws.protocoltests.restxml#RestXml") val contents = getFileContents(context.manifest, "/RestXml/RestXmlProtocolClient.swift") val expectedContents = """ - public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput - { + public func idempotencyTokenWithStructure(input: IdempotencyTokenWithStructureInput) async throws -> IdempotencyTokenWithStructureOutput { let context = ClientRuntime.HttpContextBuilder() .withEncoder(value: encoder) .withDecoder(value: decoder) diff --git a/smithy-swift-codegen/src/test/kotlin/ServiceGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/ServiceGeneratorTests.kt deleted file mode 100644 index 0904de0aa..000000000 --- a/smithy-swift-codegen/src/test/kotlin/ServiceGeneratorTests.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0. - */ - -import io.kotest.matchers.string.shouldContain -import io.kotest.matchers.string.shouldContainOnlyOnce -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import software.amazon.smithy.aws.traits.protocols.RestJson1Trait -import software.amazon.smithy.build.MockManifest -import software.amazon.smithy.codegen.core.SymbolProvider -import software.amazon.smithy.swift.codegen.ServiceGenerator -import software.amazon.smithy.swift.codegen.SwiftCodegenPlugin -import software.amazon.smithy.swift.codegen.SwiftDelegator -import software.amazon.smithy.swift.codegen.SwiftWriter -import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator -import software.amazon.smithy.swift.codegen.model.AddOperationShapes - -class ServiceGeneratorTests { - - private val commonTestContents: String - - init { - var model = javaClass.getResource("service-generator-test-operations.smithy").asSmithy() - val writer = SwiftWriter("test") - - val settings = model.defaultSettings() - val manifest = MockManifest() - model = AddOperationShapes.execute(model, settings.getService(model), settings.moduleName) - val provider: SymbolProvider = SwiftCodegenPlugin.createSymbolProvider(model, model.defaultSettings()) - val writers = SwiftDelegator(settings, model, manifest, provider) - val protocolGenerationContext = ProtocolGenerator.GenerationContext(settings, model, model.serviceShapes.first(), provider, listOf(), RestJson1Trait.ID, writers) - val generator = ServiceGenerator( - settings, - model, - provider, - writer, - writers, - protocolGenerationContext = protocolGenerationContext - ) - generator.render() - - commonTestContents = writer.toString() - } - - @Test - fun `it renders swift protocols in separate file`() { - val model = javaClass.getResource("service-generator-test-operations.smithy").asSmithy() - val manifest = MockManifest() - val context = buildMockPluginContext(model, manifest) - - SwiftCodegenPlugin().execute(context) - - Assertions.assertTrue(manifest.hasFile("example/ExampleClientProtocol.swift")) - } - - @Test - fun `it has header`() { - commonTestContents.shouldContain(SwiftWriter.staticHeader) - } - - @Test - fun `it has swift protocol signature`() { - commonTestContents.shouldContainOnlyOnce("public protocol ExampleClientProtocol {") - } - - @Test - fun `it has dependency on client runtime`() { - commonTestContents.shouldContainOnlyOnce("import ClientRuntime") - } - - @Test - fun `it renders swift func signatures correctly`() { - val expectedSignatures = listOf( - "func getFooStreamingInput(input: GetFooStreamingInputInput) async throws -> GetFooStreamingInputOutput", - "func getFooNoOutput(input: GetFooNoOutputInput) async throws -> GetFooNoOutputOutput", - "func getFooStreamingOutput(input: GetFooStreamingOutputInput) async throws -> GetFooStreamingOutputOutput", - "func getFoo(input: GetFooInput) async throws -> GetFooOutput", - "func getFooNoInput(input: GetFooNoInputInput) async throws -> GetFooNoInputOutput", - "func getFooStreamingInputNoOutput(input: GetFooStreamingInputNoOutputInput) async throws -> GetFooStreamingInputNoOutputOutput", - "func getFooStreamingOutputNoInput(input: GetFooStreamingOutputNoInputInput) async throws -> GetFooStreamingOutputNoInputOutput" - ) - expectedSignatures.forEach { - commonTestContents.shouldContainOnlyOnce(it) - } - } - - @Test - fun `it syntactic sanity checks`() { - // sanity check since we are testing fragments - var openBraces = 0 - var closedBraces = 0 - var openParens = 0 - var closedParens = 0 - commonTestContents.forEach { - when (it) { - '{' -> openBraces++ - '}' -> closedBraces++ - '(' -> openParens++ - ')' -> closedParens++ - } - } - Assertions.assertEquals(openBraces, closedBraces) - Assertions.assertEquals(openParens, closedParens) - } - - @Test - fun `deprecated trait on an operation`() { - val model = javaClass.getResource("service-generator-test-operations.smithy").asSmithy() - val manifest = MockManifest() - val context = buildMockPluginContext(model, manifest) - SwiftCodegenPlugin().execute(context) - - val exampleClientProtocol = manifest - .getFileString("example/ExampleClientProtocol.swift").get() - val operationWithDeprecatedTrait = """ - @available(*, deprecated) - """.trimIndent() - exampleClientProtocol.shouldContain(operationWithDeprecatedTrait) - } -} diff --git a/smithy-swift-codegen/src/test/kotlin/waiters/WaiterGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/waiters/WaiterGeneratorTests.kt index a7745a2d0..8c32ad4e3 100644 --- a/smithy-swift-codegen/src/test/kotlin/waiters/WaiterGeneratorTests.kt +++ b/smithy-swift-codegen/src/test/kotlin/waiters/WaiterGeneratorTests.kt @@ -26,8 +26,8 @@ class WaiterGeneratorTests { val context = setupTests("waiters.smithy", "com.test#TestHasWaiters") val contents = getFileContents(context.manifest, "/Test/Waiters.swift") val expected = """ - extension TestClientProtocol { - """.trimIndent() +extension TestClient { +""" contents.shouldContainOnlyOnce(expected) }