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

fix: Support custom error format for Route53 ChangeResourceRecordSets operation #791

Merged
merged 14 commits into from
Jan 5, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package software.amazon.smithy.aws.swift.codegen.customization.route53

import software.amazon.smithy.aws.swift.codegen.restxml.AWSRestXMLHttpResponseBindingErrorGenerator
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.swift.codegen.SwiftDelegator
import software.amazon.smithy.swift.codegen.SwiftDependency
import software.amazon.smithy.swift.codegen.SwiftSettings
import software.amazon.smithy.swift.codegen.SwiftWriter
import software.amazon.smithy.swift.codegen.core.CodegenContext
import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator
import software.amazon.smithy.swift.codegen.integration.SectionWriter
import software.amazon.smithy.swift.codegen.integration.SectionWriterBinding
import software.amazon.smithy.swift.codegen.integration.SwiftIntegration
import software.amazon.smithy.swift.codegen.model.expectShape

class Route53InvalidBatchErrorIntegration : SwiftIntegration {
override fun enabledForService(model: Model, settings: SwiftSettings): Boolean {
return model.expectShape<ServiceShape>(settings.service).isRoute53
}

override val sectionWriters: List<SectionWriterBinding>
get() = listOf(
SectionWriterBinding(AWSRestXMLHttpResponseBindingErrorGenerator.RestXMLResponseBindingSectionId, httpResponseBindingErrorGenerator)
)

private val httpResponseBindingErrorGenerator = SectionWriter { writer, previousCode ->
val operationErrorName = writer.getContext("operationErrorName") as String
if (operationErrorName == "ChangeResourceRecordSetsOutputError") {
writer.openBlock("if let customBatchError = CustomInvalidBatchError.makeFromHttpResponse(httpResponse) {", "}") {
writer.openBlock("let invalidChangeBatchError = InvalidChangeBatch(", ")") {
writer.write("customError: customBatchError,")
writer.write("headers: httpResponse.headers,")
writer.write("statusCode: httpResponse.statusCode")
}
writer.write("self = .invalidChangeBatch(invalidChangeBatchError)")
writer.write("return")
}
}
writer.write(previousCode)
}

override fun writeAdditionalFiles(ctx: CodegenContext, protocolGenerationContext: ProtocolGenerator.GenerationContext, delegator: SwiftDelegator) {
delegator.useFileWriter("${ctx.settings.moduleName}/models/ChangeResourceRecordSetsOutputError+Customization.swift") { writer ->
writer.addImport(SwiftDependency.CLIENT_RUNTIME.target)
renderCustomInvalidBatchError(writer)
renderInvalidChangeBatch(writer)
Comment on lines +46 to +47
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be awesome to have a way to just write this in swift, since it isn't dependent on the smithy model.
I filed this issue to investigate some mechanism that would allow us to write service specific swift code. I imagine the solution here will be related (if not the same) as solving the service specific integration test problem.

}
}

private fun renderCustomInvalidBatchError(writer: SwiftWriter) {
writer.openBlock("struct CustomInvalidBatchError: Decodable {", "}") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public APIs/structs must have docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't public. This is just internal as it's just used for decoding and then it's transformed into a InvalidChangeBatch instance.

writer.openBlock("struct Message: Decodable {", "}") {
writer.write("let message: String")
writer.openBlock("enum CodingKeys: String, CodingKey {", "}") {
writer.write("case message = \"Message\"")
}
}
writer.write("let requestId: String")
writer.write("let messages: [String]?")
writer.openBlock("enum CodingKeys: String, CodingKey {", "}") {
writer.write("case messages = \"Messages\"")
writer.write("case requestId = \"RequestId\"")
}
writer.openBlock("init(from decoder: Decoder) throws {", "}") {
writer.write("let container = try decoder.container(keyedBy: CodingKeys.self)")
writer.write("self.requestId = try container.decode(String.self, forKey: .requestId)")
writer.write("let messages = try container.decodeIfPresent([Message].self, forKey: .messages)")
writer.write("self.messages = messages?.map(\\.message)")
}
writer.openBlock("static func makeFromHttpResponse(_ httpResponse: ClientRuntime.HttpResponse) -> CustomInvalidBatchError? {", "}") {
writer.openBlock("guard let data = httpResponse.body.toBytes()?.getData() else {", "}") {
writer.write("return nil")
}
writer.write("return try? XMLDecoder().decode(CustomInvalidBatchError.self, from: data)")
}
}
}

private fun renderInvalidChangeBatch(writer: SwiftWriter) {
writer.openBlock("extension InvalidChangeBatch {", "}") {
writer.openBlock("init(customError: CustomInvalidBatchError, headers: Headers?, statusCode: HttpStatusCode?) {", "}") {
writer.write("self.init(messages: customError.messages)")
writer.write("self._requestID = customError.requestId")
writer.write("self._headers = headers")
writer.write("self._statusCode = statusCode")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package software.amazon.smithy.aws.swift.codegen.customization.route53

import software.amazon.smithy.aws.swift.codegen.sdkId
import software.amazon.smithy.model.shapes.ServiceShape

val ServiceShape.isRoute53: Boolean
get() = sdkId.lowercase() == "route53"
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package software.amazon.smithy.aws.swift.codegen.customization.route53
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.traits.HttpLabelTrait
Expand All @@ -12,13 +13,12 @@ import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator
import software.amazon.smithy.swift.codegen.integration.SwiftIntegration
import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils
import software.amazon.smithy.swift.codegen.middleware.OperationMiddleware
import software.amazon.smithy.swift.codegen.model.expectShape
import software.amazon.smithy.swift.codegen.model.hasTrait

private val Route53ShapeId: ShapeId = ShapeId.from("com.amazonaws.route53#AWSDnsV20130401")

class Route53TrimHostedZone : SwiftIntegration {
override fun enabledForService(model: Model, settings: SwiftSettings): Boolean {
return settings.service == Route53ShapeId
return model.expectShape<ServiceShape>(settings.service).isRoute53
}
override fun preprocessModel(model: Model, settings: SwiftSettings): Model {
return ModelTransformer.create().mapShapes(model) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ software.amazon.smithy.aws.swift.codegen.AddProtocols
software.amazon.smithy.aws.swift.codegen.customization.s3.S3ErrorIntegration
software.amazon.smithy.aws.swift.codegen.customization.s3.S3Expires
software.amazon.smithy.aws.swift.codegen.customization.route53.Route53TrimHostedZone
software.amazon.smithy.aws.swift.codegen.customization.route53.Route53InvalidBatchErrorIntegration
software.amazon.smithy.aws.swift.codegen.customization.apigateway.ApiGatewayAddAcceptHeader
software.amazon.smithy.aws.swift.codegen.customization.glacier.GlacierAddVersionHeader
software.amazon.smithy.aws.swift.codegen.customization.glacier.GlacierAccountIdDefault
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package software.amazon.smithy.aws.swift.codegen.customizations

import io.kotest.matchers.string.shouldContainOnlyOnce
import org.junit.jupiter.api.Test
import software.amazon.smithy.aws.swift.codegen.TestContext
import software.amazon.smithy.aws.swift.codegen.TestContextGenerator
import software.amazon.smithy.aws.swift.codegen.shouldSyntacticSanityCheck
import software.amazon.smithy.aws.traits.protocols.RestXmlTrait

class Route53InvalidBatchErrorIntegrationTests {

@Test
fun `001 test additional structs and extensions are generated`() {
val context = setupTests("route53-invalidbatch.smithy", "com.amazonaws.route53#Route53")
val contents = TestContextGenerator.getFileContents(context.manifest, "/Example/models/ChangeResourceRecordSetsOutputError+Customization.swift")
contents.shouldSyntacticSanityCheck()
val expectedContents =
"""
struct CustomInvalidBatchError: Decodable {
struct Message: Decodable {
let message: String
enum CodingKeys: String, CodingKey {
case message = "Message"
}
}
let requestId: String
let messages: [String]?
enum CodingKeys: String, CodingKey {
case messages = "Messages"
case requestId = "RequestId"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.requestId = try container.decode(String.self, forKey: .requestId)
let messages = try container.decodeIfPresent([Message].self, forKey: .messages)
self.messages = messages?.map(\.message)
}
static func makeFromHttpResponse(_ httpResponse: ClientRuntime.HttpResponse) -> CustomInvalidBatchError? {
guard let data = httpResponse.body.toBytes()?.getData() else {
return nil
}
return try? XMLDecoder().decode(CustomInvalidBatchError.self, from: data)
}
}
extension InvalidChangeBatch {
init(customError: CustomInvalidBatchError, headers: Headers?, statusCode: HttpStatusCode?) {
self.init(messages: customError.messages)
self._requestID = customError.requestId
self._headers = headers
self._statusCode = statusCode
}
}
""".trimIndent()
contents.shouldContainOnlyOnce(expectedContents)
}

@Test
fun `002 test ChangeResourceRecordSetsOutputError+HttpResponseBinding is customized`() {
val context = setupTests("route53-invalidbatch.smithy", "com.amazonaws.route53#Route53")
val contents = TestContextGenerator.getFileContents(context.manifest, "/Example/models/ChangeResourceRecordSetsOutputError+HttpResponseBinding.swift")
contents.shouldSyntacticSanityCheck()
val expectedContents =
"""
extension ChangeResourceRecordSetsOutputError: ClientRuntime.HttpResponseBinding {
public init(httpResponse: ClientRuntime.HttpResponse, decoder: ClientRuntime.ResponseDecoder? = nil) throws {
if let customBatchError = CustomInvalidBatchError.makeFromHttpResponse(httpResponse) {
let invalidChangeBatchError = InvalidChangeBatch(
customError: customBatchError,
headers: httpResponse.headers,
statusCode: httpResponse.statusCode
)
self = .invalidChangeBatch(invalidChangeBatchError)
return
}
let errorDetails = try AWSClientRuntime.RestXMLError(httpResponse: httpResponse)
try self.init(errorType: errorDetails.errorCode, httpResponse: httpResponse, decoder: decoder, message: errorDetails.message, requestID: errorDetails.requestId)
}
}
""".trimIndent()
contents.shouldContainOnlyOnce(expectedContents)
}

private fun setupTests(smithyFile: String, serviceShapeId: String): TestContext {
val context = TestContextGenerator.initContextFrom(smithyFile, serviceShapeId, RestXmlTrait.ID)
return context
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
$version: "1.0"
namespace com.amazonaws.route53

use aws.api#service
use aws.protocols#restXml

@service(sdkId: "Route53")
@restXml
service Route53 {
version: "2019-12-16",
operations: [ChangeResourceRecordSets]
}

@http(uri: "/ChangeResourceRecordSets", method: "POST")
operation ChangeResourceRecordSets {
input: InputOutput
output: InputOutput
errors: [InvalidChangeBatch]
}

structure InputOutput {
foo: String
}

@error("client")
structure InvalidChangeBatch {
message: String
}