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: s3 custom treatment getbucketlocation response #989

Merged
merged 21 commits into from
Aug 8, 2023

Conversation

0marperez
Copy link
Contributor

@0marperez 0marperez commented Jul 19, 2023

Issue #

194 - Custom treatment of GetBucketLocation response

Description of changes

This was previously fixed using a customization but it left out error deserialization and deserialization of both wrapped and unwrapped non-error responses. Both possibilities are accounted for now

Also look at: smithy-lang/smithy-kotlin#897

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Copy link
Collaborator

@aajtodd aajtodd left a comment

Choose a reason for hiding this comment

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

I think we should fix this by implementing support generically for @s3UnwrappedXmlOutput trait.

This could be done by:

  1. Add a new FieldTrait for XmlUnwrappedStruct or similar and implement support in the XmlDeserializer for it.
  2. Either override the descriptor generator for the S3 generator OR define our own generic trait for this and map the s3UnwrappedXmlOutput to our own generic trait. Then smithy-kotlin just operates on our own trait and a customization can preprocess the model to map s3UnwrappedXmlOutput to whatever we are looking for/define.

* Applies the [UnwrappedXMLOutput] custom-made [annotation trait](https://smithy.io/2.0/spec/model.html?highlight=annotation#annotation-traits) to a manually-curated list of structures
* to mark when special unwrapped xml output deserialization is required.
*/
class UnwrappedXMLOutputIntegration : KotlinIntegration {
Copy link
Contributor

Choose a reason for hiding this comment

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

Style: Generally our names should follow PascalCase, including treating abbreviations like "XML" as regular words for the purposes of capitalization (e.g., UnwrappedXmlOutputIntegration).

Comment on lines 31 to 37
when {
shape.id.toString() in SHAPE_IDS -> {
check(shape is StructureShape) { "Cannot apply UnwrappedXMLOutput to non-member shape" }
shape.toBuilder().addTrait(UnwrappedXMLOutput()).build()
}
else -> shape
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Why do we need to check shape IDs from an allowlist? Why can't we just detect the aws.customizations#s3UnwrappedXmlOutput trait and then apply the new UnwrappedXMLOutput trait?

when {
    shape is StructureShape && shape.hasTrait<S3UnwrappedXmlOutputTrait>() ->
        shape.toBuilder().addTrait(UnwrappedXMLOutput()).build()

    else -> shape
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah that seems cleaner.

Comment on lines 31 to 37
when {
shape.id.toString() in SHAPE_IDS -> {
check(shape is StructureShape) { "Cannot apply UnwrappedXMLOutput to non-member shape" }
shape.toBuilder().addTrait(UnwrappedXMLOutput()).build()
}
else -> shape
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah that seems cleaner.

import aws.smithy.kotlin.runtime.serde.xml.XmlNamespace
import aws.smithy.kotlin.runtime.serde.xml.XmlSerialName

internal fun deserializeWrapped(builder: GetBucketLocationResponse.Builder, payload: ByteArray) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

fix: We shouldn't need this now right?

* Registers an integration that replaces the codegened GetBucketLocationDeserializer with
* custom deserialization logic.
* Registers an integration that tries deserializing the `GetBucketLocationOperation`
* response as wrapped XML response when doing so as [unwrapped](https://smithy.io/2.0/aws/customizations/s3-customizations.html#aws-customizations-s3unwrappedxmloutput-trait) is unsuccessful.
*/
class GetBucketLocationDeserializerIntegration : KotlinIntegration {
Copy link
Collaborator

Choose a reason for hiding this comment

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

fix: Shouldn't need this now either right?

Comment on lines 27 to 52
override fun preprocessModel(model: Model, settings: KotlinSettings): Model {
val unwrappedStructures = mutableSetOf<String>()
ModelTransformer
.create()
.mapShapes(model) { shape ->
when {
shape is OperationShape && shape.hasTrait<S3UnwrappedXmlOutputTrait>() -> {
if (shape.outputShape.toString() != "smithy.api#Unit") unwrappedStructures.add(shape.outputShape.toString())
shape
}
else -> shape
}
}

return ModelTransformer
.create()
.mapShapes(model) { shape ->
when {
shape.id.toString() in unwrappedStructures -> {
check(shape is StructureShape) { "Cannot apply UnwrappedXMLOutput to non-structure shape" }
shape.toBuilder().addTrait(UnwrappedXmlOutput()).build()
}
else -> shape
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment: We don't need a full mapShapes call to find the initial set of structs. We can just iterate through the model directly:

override fun preprocessModel(model: Model, settings: KotlinSettings) {
    val unwrappedStructures = model
        .operationShapes
        .filter { it.hasTrait<S3UnwrappedXmlOutputTrait>() }
        .map { it.outputShape }
        .toSet()
    return ModelTransformer
        .create()
        .mapShapes(model) { shape ->
            when {
                shape.id in unwrappedStructures ->
                    shape.toBuilder().addTrait(UnwrappedXmlOutput()).build()
                else -> shape
            }
        }
}

Comment on lines 34 to 35
if (shape.outputShape.toString() != "smithy.api#Unit") unwrappedStructures.add(shape.outputShape.toString())
shape
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Is checking for outputShape != smithy.api#Unit important here? It seems like that would be a malformed model since there's no output to unwrap.

Comment on lines 46 to 47
check(shape is StructureShape) { "Cannot apply UnwrappedXMLOutput to non-structure shape" }
shape.toBuilder().addTrait(UnwrappedXmlOutput()).build()
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Is checking for shape is StructureShape important here? It seems like the only things we add to the list are output shapes from operations marked with S3UnwrappedXmlOutputTrait, which must be structures right?

Comment on lines 22 to 39
fun deserializeUnwrappedResponse() {
val responseXML = """
<?xml version="1.0" encoding="UTF-8"?>
<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">us-west-2</LocationConstraint>
""".trimIndent()

val response: HttpResponse = HttpResponse(
HttpStatusCode(200, "Success"),
Headers.invoke { },
HttpBody.fromBytes(responseXML.encodeToByteArray()),
)

val actual = runBlocking {
GetBucketLocationOperationDeserializer().deserialize(ExecutionContext(), response)
}

assertEquals("UsWest2", actual.locationConstraint.toString())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: I'm confused by this test. Why is the expected output UsWest2 when response XML stated us-west-2?

}

@Test
fun test2() {
Copy link
Member

Choose a reason for hiding this comment

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

Can these test and test2 be given more precise names?

Comment on lines 34 to 44
// Test s3 model where only the service name matters
private fun model(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()
Copy link
Member

Choose a reason for hiding this comment

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

You can grab this function from https://github.com/awslabs/aws-sdk-kotlin/blob/s3-custom-treatment-getbucketlocation-response/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/aws/sdk/kotlin/codegen/testutil/ModelUtil.kt#L7 instead of duplicating it

Note this wasn't available when you first created this PR, I merged it in sometime after :)

@sonarcloud
Copy link

sonarcloud bot commented Aug 7, 2023

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

No Coverage information No Coverage information
0.0% 0.0% Duplication

@0marperez 0marperez merged commit f8c9121 into main Aug 8, 2023
11 of 12 checks passed
@0marperez 0marperez deleted the s3-custom-treatment-getbucketlocation-response branch August 8, 2023 15:36
@aajtodd aajtodd mentioned this pull request Sep 5, 2023
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants