Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add WaiterTypedError type, conform operation errors to it #491

Merged
merged 8 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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 }
}
16 changes: 16 additions & 0 deletions Packages/ClientRuntime/Sources/Networking/SdkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,19 @@ public enum SdkError<E>: 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
}
}
}
16 changes: 16 additions & 0 deletions Packages/ClientRuntime/Sources/Waiters/WaiterTypedError.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("}")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AwsQueryErrorTrait>()?.let {
return it.code
} ?: run {
return ctx.symbolProvider.toSymbol(errorShape).name
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
32 changes: 32 additions & 0 deletions smithy-swift-codegen/src/test/resources/waiter-typed-error.smithy
Original file line number Diff line number Diff line change
@@ -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
}