From e2b95fac6aacbdbb5d94ea41512e16f6f9a34d89 Mon Sep 17 00:00:00 2001 From: Josh Elkins Date: Thu, 8 Dec 2022 17:06:05 -0600 Subject: [PATCH] feat: Add WaiterTypedError type, conform operation errors to it (#491) --- .../Http/UnknownHttpServiceError.swift | 19 ++++- .../Sources/Networking/SdkError.swift | 16 ++++ .../Sources/Waiters/WaiterTypedError.swift | 16 ++++ ...HttpResponseBindingErrorNarrowGenerator.kt | 2 +- .../httpResponse/HttpResponseGenerator.kt | 1 + .../httpResponse/WaiterTypedErrorGenerator.kt | 74 +++++++++++++++++++ .../kotlin/WaiterTypedErrorGeneratorTests.kt | 39 ++++++++++ .../test/resources/waiter-typed-error.smithy | 32 ++++++++ 8 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 Packages/ClientRuntime/Sources/Waiters/WaiterTypedError.swift create mode 100644 smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/WaiterTypedErrorGenerator.kt create mode 100644 smithy-swift-codegen/src/test/kotlin/WaiterTypedErrorGeneratorTests.kt create mode 100644 smithy-swift-codegen/src/test/resources/waiter-typed-error.smithy diff --git a/Packages/ClientRuntime/Sources/Networking/Http/UnknownHttpServiceError.swift b/Packages/ClientRuntime/Sources/Networking/Http/UnknownHttpServiceError.swift index 69b436105..b8f52603b 100644 --- a/Packages/ClientRuntime/Sources/Networking/Http/UnknownHttpServiceError.swift +++ b/Packages/ClientRuntime/Sources/Networking/Http/UnknownHttpServiceError.swift @@ -5,6 +5,8 @@ /// General Service Error structure used when exact error could not be deduced from the `HttpResponse` public struct UnknownHttpServiceError: HttpServiceError, Swift.Equatable { + public var _errorType: String? + public var _isThrottling: Bool = false public var _statusCode: HttpStatusCode? @@ -19,9 +21,24 @@ public struct UnknownHttpServiceError: HttpServiceError, Swift.Equatable { } extension UnknownHttpServiceError { - public init(httpResponse: HttpResponse, message: String? = nil) { + + + /// Creates an `UnknownHttpServiceError` from a HTTP response. + /// - Parameters: + /// - httpResponse: The `HttpResponse` for this error. + /// - message: The message associated with this error. Defaults to `nil`. + /// - errorType: The error type associated with this error. Defaults to `nil`. + public init(httpResponse: HttpResponse, message: String? = nil, errorType: String? = nil) { self._statusCode = httpResponse.statusCode self._headers = httpResponse.headers self._message = message + self._errorType = errorType } } + +extension UnknownHttpServiceError: WaiterTypedError { + + /// The Smithy identifier, without namespace, for the type of this error, or `nil` if the + /// error has no known type. + public var waiterErrorType: String? { _errorType } +} diff --git a/Packages/ClientRuntime/Sources/Networking/SdkError.swift b/Packages/ClientRuntime/Sources/Networking/SdkError.swift index 6f97c14d2..f6cd8bb81 100644 --- a/Packages/ClientRuntime/Sources/Networking/SdkError.swift +++ b/Packages/ClientRuntime/Sources/Networking/SdkError.swift @@ -14,3 +14,19 @@ public enum SdkError: Error { case unknown(Error?) } + +extension SdkError: WaiterTypedError { + + /// The Smithy identifier, without namespace, for the type of this error, or `nil` if the + /// error has no known type. + public var waiterErrorType: String? { + switch self { + case .service(let error, _): + return (error as? WaiterTypedError)?.waiterErrorType + case .client(let error, _): + return (error as? WaiterTypedError)?.waiterErrorType + case .unknown(let error): + return (error as? WaiterTypedError)?.waiterErrorType + } + } +} diff --git a/Packages/ClientRuntime/Sources/Waiters/WaiterTypedError.swift b/Packages/ClientRuntime/Sources/Waiters/WaiterTypedError.swift new file mode 100644 index 000000000..fe5baaf47 --- /dev/null +++ b/Packages/ClientRuntime/Sources/Waiters/WaiterTypedError.swift @@ -0,0 +1,16 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +import Foundation + +/// An error that may be identified by a string error type, for the purpose of matching the error to a Smithy `errorType` acceptor. +/// This protocol will only be extended onto errors that have a Smithy waiter defined for them, and is only intended for use in the +/// operation of a waiter. +public protocol WaiterTypedError: Error { + + /// The Smithy identifier, without namespace, for the type of this error, or `nil` if the + /// error has no known type. + var waiterErrorType: String? { get } +} diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HttpResponseBindingErrorNarrowGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HttpResponseBindingErrorNarrowGenerator.kt index 1de3a71e7..80c95a4d0 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HttpResponseBindingErrorNarrowGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HttpResponseBindingErrorNarrowGenerator.kt @@ -58,7 +58,7 @@ class HttpResponseBindingErrorNarrowGenerator( var errorShapeEnumCase = errorShapeType.decapitalize() writer.write("case \$S : self = .\$L(try \$L(httpResponse: httpResponse, decoder: decoder, message: message, requestID: requestID))", errorShapeName, errorShapeEnumCase, errorShapeType) } - writer.write("default : self = .unknown($unknownServiceErrorType(httpResponse: httpResponse, message: message, requestID: requestID))") + writer.write("default : self = .unknown($unknownServiceErrorType(httpResponse: httpResponse, message: message, requestID: requestID, errorType: errorType))") writer.write("}") } } diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HttpResponseGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HttpResponseGenerator.kt index ac892d384..1d7becfc7 100644 --- a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HttpResponseGenerator.kt +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/HttpResponseGenerator.kt @@ -36,6 +36,7 @@ class HttpResponseGenerator( httpOperations.forEach { httpResponseBindingErrorGenerator.render(ctx, it) HttpResponseBindingErrorNarrowGenerator(ctx, it, unknownServiceErrorSymbol).render() + WaiterTypedErrorGenerator(ctx, it, unknownServiceErrorSymbol).render() } val modeledErrors = httpOperations.flatMap { it.errors }.map { ctx.model.expectShape(it) as StructureShape }.toSet() diff --git a/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/WaiterTypedErrorGenerator.kt b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/WaiterTypedErrorGenerator.kt new file mode 100644 index 000000000..0634f1fa5 --- /dev/null +++ b/smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/integration/httpResponse/WaiterTypedErrorGenerator.kt @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +package software.amazon.smithy.swift.codegen.integration.httpResponse + +import software.amazon.smithy.aws.traits.protocols.AwsQueryErrorTrait +import software.amazon.smithy.codegen.core.Symbol +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.swift.codegen.SwiftDependency +import software.amazon.smithy.swift.codegen.declareSection +import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator +import software.amazon.smithy.swift.codegen.integration.SectionId +import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils +import software.amazon.smithy.swift.codegen.model.getTrait + +class WaiterTypedErrorGenerator( + val ctx: ProtocolGenerator.GenerationContext, + val op: OperationShape, + val unknownServiceErrorSymbol: Symbol +) { + object WaiterTypedErrorGeneratorSectionId : SectionId + + fun render() { + val errorShapes = op.errors.map { ctx.model.expectShape(it) as StructureShape }.toSet().sorted() + val operationErrorName = MiddlewareShapeUtils.outputErrorSymbolName(op) + val rootNamespace = ctx.settings.moduleName + val httpBindingSymbol = Symbol.builder() + .definitionFile("./$rootNamespace/models/$operationErrorName+WaiterTypedError.swift") + .name(operationErrorName) + .build() + + ctx.delegator.useShapeWriter(httpBindingSymbol) { writer -> + writer.addImport(SwiftDependency.CLIENT_RUNTIME.target) + writer.addImport(unknownServiceErrorSymbol) + val unknownServiceErrorType = unknownServiceErrorSymbol.name + + val context = mapOf( + "ctx" to ctx, + "unknownServiceErrorType" to unknownServiceErrorType, + "operationErrorName" to operationErrorName, + "errorShapes" to errorShapes + ) + writer.declareSection(WaiterTypedErrorGeneratorSectionId, context) { + writer.openBlock("extension \$L: WaiterTypedError {", "}", operationErrorName) { + writer.write("") + writer.write("/// The Smithy identifier, without namespace, for the type of this error, or `nil` if the") + writer.write("/// error has no known type.") + writer.openBlock("public var waiterErrorType: String? {", "}") { + writer.write("switch self {") + for (errorShape in errorShapes) { + var errorShapeName = resolveErrorShapeName(errorShape) + var errorShapeType = ctx.symbolProvider.toSymbol(errorShape).name + var errorShapeEnumCase = errorShapeType.decapitalize() + writer.write("case .\$L: return \$S", errorShapeEnumCase, errorShapeName) + } + writer.write("case .unknown(let error): return error.waiterErrorType") + writer.write("}") + } + } + } + } + } + + private fun resolveErrorShapeName(errorShape: StructureShape): String { + errorShape.getTrait()?.let { + return it.code + } ?: run { + return ctx.symbolProvider.toSymbol(errorShape).name + } + } +} diff --git a/smithy-swift-codegen/src/test/kotlin/WaiterTypedErrorGeneratorTests.kt b/smithy-swift-codegen/src/test/kotlin/WaiterTypedErrorGeneratorTests.kt new file mode 100644 index 000000000..c2326f89c --- /dev/null +++ b/smithy-swift-codegen/src/test/kotlin/WaiterTypedErrorGeneratorTests.kt @@ -0,0 +1,39 @@ +import io.kotest.matchers.string.shouldContainOnlyOnce +import org.junit.jupiter.api.Test +import software.amazon.smithy.swift.codegen.ClientRuntimeTypes +import software.amazon.smithy.swift.codegen.integration.httpResponse.WaiterTypedErrorGenerator + +class WaiterTypedErrorGeneratorTests { + + @Test + fun `renders correct WaiterTypedError extension for operation error`() { + val context = setupTests("waiter-typed-error.smithy", "com.test#WaiterTypedErrorTest") + val contents = getFileContents(context.manifest, "/WaiterTypedErrorTest/models/GetWidgetOutputError+WaiterTypedError.swift") + val expected = """ + extension GetWidgetOutputError: WaiterTypedError { + + /// The Smithy identifier, without namespace, for the type of this error, or `nil` if the + /// error has no known type. + public var waiterErrorType: String? { + switch self { + case .invalidWidgetError: return "InvalidWidgetError" + case .widgetNotFoundError: return "WidgetNotFoundError" + case .unknown(let error): return error.waiterErrorType + } + } + } + """.trimIndent() + contents.shouldContainOnlyOnce(expected) + } + + private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext { + val context = TestContext.initContextFrom(smithyFile, serviceShapeId, MockHttpRestJsonProtocolGenerator()) { model -> + model.defaultSettings(serviceShapeId, "WaiterTypedErrorTest", "2019-12-16", "WaiterTypedErrorTest") + } + context.generator.generateProtocolClient(context.generationCtx) + val operationShape = context.generationCtx.model.operationShapes.first() + WaiterTypedErrorGenerator(context.generationCtx, operationShape, ClientRuntimeTypes.Http.UnknownHttpServiceError).render() + context.generationCtx.delegator.flushWriters() + return context + } +} diff --git a/smithy-swift-codegen/src/test/resources/waiter-typed-error.smithy b/smithy-swift-codegen/src/test/resources/waiter-typed-error.smithy new file mode 100644 index 000000000..85ec6d6cf --- /dev/null +++ b/smithy-swift-codegen/src/test/resources/waiter-typed-error.smithy @@ -0,0 +1,32 @@ +$version: "1.0" + +namespace com.test + +use aws.api#service +use aws.protocols#restJson1 + +@service(sdkId: "WaiterTypedErrorTest") +service WaiterTypedErrorTest { + version: "2019-12-16", + operations: [GetWidget] +} + +@http(uri: "/GetWidget", method: "GET") +operation GetWidget { + output: GetWidgetOutput + errors: [WidgetNotFoundError, InvalidWidgetError] +} + +structure GetWidgetOutput { + name: String +} + +@error("client") +structure WidgetNotFoundError { + name: String +} + +@error("client") +structure InvalidWidgetError { + name: String +}