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: route53 custom error unmarshalling #961

Merged
merged 25 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6de9a9a
Added custom unmarshaller & wrote one test
0marperez Jun 21, 2023
68d1cf4
Fixed unmarshalling & added tests
0marperez Jun 23, 2023
6983ee2
Run ktlint
0marperez Jun 23, 2023
a5545c9
Added change log
0marperez Jun 23, 2023
87a497d
Renamed files and modified change log
0marperez Jun 27, 2023
78a50ee
Removed code duplication in favor of hooks and preprocessing
0marperez Jun 27, 2023
4283403
Merge branch 'main' of https://github.com/awslabs/aws-sdk-kotlin into…
0marperez Jun 28, 2023
dd81188
Bug fixes, ktlint & integration tests
0marperez Jun 28, 2023
f3ee41e
Added copyright header missing
0marperez Jun 28, 2023
bc422b3
Polishing: visibility modifiers and legibility
0marperez Jun 29, 2023
24e7975
Made changes to integration
0marperez Jun 29, 2023
9cb4e0c
Got rid of unnecessary data class and interface
0marperez Jun 29, 2023
dc8ab39
Merge branch 'main' of https://github.com/awslabs/aws-sdk-kotlin into…
0marperez Jun 29, 2023
648d6ca
Revert "Made changes to integration"
0marperez Jun 29, 2023
f19a940
Revert "Got rid of unnecessary data class and interface"
0marperez Jun 29, 2023
c79e34e
fix section writer assumption
0marperez Jun 29, 2023
5ec392a
Added back invalid change batch deserializer
0marperez Jun 29, 2023
f381c29
Renamed unused parameter
0marperez Jun 29, 2023
c3be1e9
Merge branch 'main' of https://github.com/awslabs/aws-sdk-kotlin into…
0marperez Jun 30, 2023
6fc5cf4
Changed Route53Exception to InvalidChangeBatch type
0marperez Jun 30, 2023
9fd40be
Added multiple message support and some clean up
0marperez Jul 3, 2023
b7d2597
Changed separator for multiple messages
0marperez Jul 5, 2023
678c896
Refactoring and correctnes addressed based on PR feedback
0marperez Jul 7, 2023
212fb85
Merge branch 'main' of https://github.com/awslabs/aws-sdk-kotlin into…
0marperez Jul 7, 2023
a21df0b
nit fix
0marperez Jul 7, 2023
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
8 changes: 8 additions & 0 deletions .changes/7f80de87-a59a-420e-a175-c21a89f93fe9.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "7f80de87-a59a-420e-a175-c21a89f93fe9",
"type": "bugfix",
"description": "Correctly handle and throw `InvalidChangeBatch` responses from Route53",
"issues": [
"awslabs/aws-sdk-kotlin#242"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.codegen.customization.route53

import aws.sdk.kotlin.codegen.protocols.core.AwsHttpBindingProtocolGenerator
import aws.sdk.kotlin.codegen.sdkId
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
import software.amazon.smithy.kotlin.codegen.model.expectShape
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.transform.ModelTransformer

class ChangeResourceRecordSetsUnmarshallingIntegration : KotlinIntegration {
override val sectionWriters: List<SectionWriterBinding> = listOf(
SectionWriterBinding(AwsHttpBindingProtocolGenerator.ProtocolErrorDeserialization) { writer, originalValue ->
val lines = originalValue?.split("\n")
writer.write("#L\naws.sdk.kotlin.services.route53.internal.parseCustomXmlErrorResponse(payload) ?: #L", lines?.get(0) ?: "", lines?.get(1) ?: "")
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Solely referring to the original value with this lines list is pretty inscrutable. I suggest giving each line a clearer name so that it's easier to understand what's going on here. We can also clean up the null checking by just requiring a non-null value up front:

requireNotNull(originalValue) { "Unexpectedly empty original value" }
val (nullCheckLine, parseLine) = originalValue.split("\n", limit = 2)
writer.write("#L", nullCheckLine)
writer.write("aws.sdk.kotlin.services.route53.internal.parseCustomXmlErrorResponse(payload) ?: #L", parseLine)

},
)

override fun preprocessModel(model: Model, settings: KotlinSettings): Model {
val transformer = ModelTransformer.create()
return transformer.mapShapes(model) { shape ->
if (shape.id.name == "ChangeResourceRecordsSet" && shape is OperationShape) {
shape.errors.removeIf { error -> error.name == "InvalidChangeBatch" }
}
shape
}
}

override fun enabledForService(model: Model, settings: KotlinSettings) =
model.expectShape<ServiceShape>(settings.service).sdkId.equals("route 53", ignoreCase = true)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import aws.sdk.kotlin.codegen.protocols.protocoltest.AwsHttpProtocolUnitTestResp
import software.amazon.smithy.aws.traits.protocols.AwsQueryCompatibleTrait
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.integration.SectionId
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
import software.amazon.smithy.kotlin.codegen.model.buildSymbol
import software.amazon.smithy.kotlin.codegen.model.hasTrait
Expand Down Expand Up @@ -99,6 +100,8 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator()
}
}

object ProtocolErrorDeserialization : SectionId

protected open fun renderThrowOperationError(
ctx: ProtocolGenerator.GenerationContext,
op: OperationShape,
Expand All @@ -110,7 +113,7 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator()
.write("")
.write("val errorDetails = try {")
.indent()
.call {
.declareSection(ProtocolErrorDeserialization) {
renderDeserializeErrorDetails(ctx, op, writer)
}
.dedent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ aws.sdk.kotlin.codegen.customization.RemoveEventStreamOperations
aws.sdk.kotlin.codegen.customization.flexiblechecksums.FlexibleChecksumsRequest
aws.sdk.kotlin.codegen.customization.flexiblechecksums.FlexibleChecksumsResponse
aws.sdk.kotlin.codegen.customization.route53.TrimResourcePrefix
aws.sdk.kotlin.codegen.customization.route53.ChangeResourceRecordSetsUnmarshallingIntegration
aws.sdk.kotlin.codegen.customization.s3.ClientConfigIntegration
aws.sdk.kotlin.codegen.customization.s3.ContinueIntegration
aws.sdk.kotlin.codegen.customization.s3.HttpPathFilter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.codegen.customization.route53

import org.junit.jupiter.api.Test
import software.amazon.smithy.kotlin.codegen.test.defaultSettings
import software.amazon.smithy.kotlin.codegen.test.prependNamespaceAndService
import software.amazon.smithy.kotlin.codegen.test.toSmithyModel
import software.amazon.smithy.model.Model
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class ChangeResourceRecordSetsUnmarshallingIntegrationTest {
@Test
fun nonRoute53ModelIntegration() {
val model = sampleModel("not route 53")
val isEnabledForModel = ChangeResourceRecordSetsUnmarshallingIntegration().enabledForService(model, model.defaultSettings())
assertFalse(isEnabledForModel)
}

@Test
fun route53ModelIntegration() {
val model = sampleModel("route 53")
val isEnabledForModel = ChangeResourceRecordSetsUnmarshallingIntegration().enabledForService(model, model.defaultSettings())
assertTrue(isEnabledForModel)
}
}

private fun sampleModel(serviceName: String): Model =
"""
@http(method: "PUT", uri: "/foo")
operation Foo { }

@http(method: "POST", uri: "/bar")
operation Bar { }
"""
.prependNamespaceAndService(operations = listOf("Foo", "Bar"), serviceName = serviceName)
.toSmithyModel()
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.services.route53.internal

import aws.smithy.kotlin.runtime.InternalApi
import aws.smithy.kotlin.runtime.awsprotocol.ErrorDetails
import aws.smithy.kotlin.runtime.serde.*
import aws.smithy.kotlin.runtime.serde.xml.XmlDeserializer
import aws.smithy.kotlin.runtime.serde.xml.XmlSerialName

@InternalApi
public suspend fun parseCustomXmlErrorResponse(payload: ByteArray): ErrorDetails? {
0marperez marked this conversation as resolved.
Show resolved Hide resolved
val details = InvalidChangeBatchDeserializer.deserialize(XmlDeserializer(payload, true))
?: InvalidChangeBatchMessageDeserializer.deserialize(XmlDeserializer(payload, true))
?: return null
return ErrorDetails(details.code, details.message, details.requestId)
}

internal object InvalidChangeBatchDeserializer {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Couldn't this and basically everything else in this file be private (except for parseCustomXmlErrorResponse)?

private val MESSAGES_DESCRIPTOR = SdkFieldDescriptor(SerialKind.Struct, XmlSerialName("Messages"))
private val REQUESTID_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, XmlSerialName("RequestId"))
private val OBJ_DESCRIPTOR = SdkObjectDescriptor.build {
trait(XmlSerialName("InvalidChangeBatch"))
field(MESSAGES_DESCRIPTOR)
field(REQUESTID_DESCRIPTOR)
}

suspend fun deserialize(deserializer: Deserializer): XmlErrorResponse? {
var requestId: String? = null
var messages: XmlError? = null

return try {
deserializer.deserializeStruct(OBJ_DESCRIPTOR) {
loop@ while (true) {
when (findNextFieldIndex()) {
MESSAGES_DESCRIPTOR.index -> messages = InvalidChangeBatchMessageDeserializer.deserialize(deserializer)
REQUESTID_DESCRIPTOR.index -> requestId = deserializeString()
null -> break@loop
else -> skipValue()
}
}
}
XmlErrorResponse(messages, requestId ?: messages?.requestId)
} catch (e: DeserializationException) {
null // return so an appropriate exception type can be instantiated above here.
}
}
}

internal object InvalidChangeBatchMessageDeserializer {
private val MESSAGE_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, XmlSerialName("Message"))
private val CODE_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, XmlSerialName("Code"))
private val REQUESTID_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, XmlSerialName("RequestId"))
private val OBJ_DESCRIPTOR = SdkObjectDescriptor.build {
trait(XmlSerialName("Messages"))
field(MESSAGE_DESCRIPTOR)
field(CODE_DESCRIPTOR)
field(REQUESTID_DESCRIPTOR)
}

suspend fun deserialize(deserializer: Deserializer): XmlError? {
var message: String? = null
var code: String? = null
var requestId: String? = null

return try {
deserializer.deserializeStruct(OBJ_DESCRIPTOR) {
loop@ while (true) {
when (findNextFieldIndex()) {
MESSAGE_DESCRIPTOR.index -> message = deserializeString()
CODE_DESCRIPTOR.index -> code = deserializeString()
REQUESTID_DESCRIPTOR.index -> requestId = deserializeString()
null -> break@loop
else -> skipValue()
}
}
}
XmlError(requestId, code, message)
} catch (e: DeserializationException) {
null // return so an appropriate exception type can be instantiated above here.
}
}
}

// XML Error response parser from RestXMLErrorDeserializer
internal interface RestXmlErrorDetails {
val requestId: String?
val code: String?
val message: String?
}

// Models "ErrorResponse" type in https://awslabs.github.io/smithy/1.0/spec/aws/aws-restxml-protocol.html#operation-error-serialization
internal data class XmlErrorResponse(
val error: XmlError?,
override val requestId: String? = error?.requestId,
) : RestXmlErrorDetails {
override val code: String? = error?.code
override val message: String? = error?.message
}

// Models "Error" type in https://awslabs.github.io/smithy/1.0/spec/aws/aws-restxml-protocol.html#operation-error-serialization
internal data class XmlError(
override val requestId: String?,
override val code: String?,
override val message: String?,
) : RestXmlErrorDetails
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Do we need these classes? Couldn't the deserializers return ErrorDetails? directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One of the deserializers can, the other can't. I can have the one that can return ErrorDetails? do that but I would have to leave the other data class in there for the one that can't.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doing that resulted in some weird errors with other parts of the code, I think I'll just leave it as is

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.sdk.kotlin.services.route53.internal

import aws.sdk.kotlin.services.route53.model.Route53Exception
import aws.sdk.kotlin.services.route53.transform.ChangeResourceRecordSetsOperationDeserializer
import aws.smithy.kotlin.runtime.http.Headers
import aws.smithy.kotlin.runtime.http.HttpBody
import aws.smithy.kotlin.runtime.http.HttpStatusCode
import aws.smithy.kotlin.runtime.http.response.HttpResponse
import aws.smithy.kotlin.runtime.operation.ExecutionContext
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import kotlin.test.assertEquals

class ChangeResourceRecordSetsUnmarshallingTest {
@Test
fun unmarshallChangeResourceRecordSetsInvaildChangeBatchResponse() {
val bodyText = """
<?xml version="1.0" encoding="UTF-8"?>
<InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<Messages>
<Message>This is a ChangeResourceRecordSets InvaildChangeBatchResponse message</Message>
</Messages>
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
</InvalidChangeBatch>
""".trimIndent()

val response: HttpResponse = HttpResponse(
HttpStatusCode(400, "Bad Request"),
Headers.invoke { },
HttpBody.fromBytes(bodyText.encodeToByteArray()),
)

val exception = assertThrows<Route53Exception> {
runBlocking {
ChangeResourceRecordSetsOperationDeserializer().deserialize(ExecutionContext(), response)
}
}
assertEquals("This is a ChangeResourceRecordSets InvaildChangeBatchResponse message", exception.message)
}

@Test
fun unmarshallChangeResourceRecordSetsGenericErrorResponse() {
val bodyText = """
<?xml version="1.0"?>
<ErrorResponse xmlns="http://route53.amazonaws.com/doc/2016-09-07/">
<Error>
<Type>Sender</Type>
<Code>MalformedXML</Code>
<Message>This is a ChangeResourceRecordSets generic error message</Message>
</Error>
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
</ErrorResponse>
""".trimIndent()

val response: HttpResponse = HttpResponse(
HttpStatusCode(400, "Bad Request"),
Headers.invoke { },
HttpBody.fromBytes(bodyText.encodeToByteArray()),
)

val exception = assertThrows<Route53Exception> {
runBlocking {
ChangeResourceRecordSetsOperationDeserializer().deserialize(ExecutionContext(), response)
}
}
assertEquals("This is a ChangeResourceRecordSets generic error message", exception.message)
}
}
Loading