Skip to content

Commit

Permalink
feat: Add WaiterTypedError type, conform operation errors to it (#491)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbelkins authored Dec 8, 2022
1 parent 37b93cc commit e2b95fa
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 2 deletions.
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
}

0 comments on commit e2b95fa

Please sign in to comment.