Skip to content

Commit

Permalink
Customize S3's GetBucketLocation call to correctly parse the response (
Browse files Browse the repository at this point in the history
…#516)

* Customize S3's GetBucketLocation call to correctly parse the response

* Move the regression test into s3-tests.smithy
  • Loading branch information
jdisanti committed Jun 18, 2021
1 parent c75a58f commit 8075c77
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
package software.amazon.smithy.rustsdk.customize.s3

import software.amazon.smithy.aws.traits.protocols.RestXmlTrait
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.shapes.ShapeId
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.transform.ModelTransformer
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.Writable
import software.amazon.smithy.rust.codegen.rustlang.asType
Expand All @@ -24,6 +28,7 @@ import software.amazon.smithy.rust.codegen.smithy.letIf
import software.amazon.smithy.rust.codegen.smithy.protocols.ProtocolMap
import software.amazon.smithy.rust.codegen.smithy.protocols.RestXml
import software.amazon.smithy.rust.codegen.smithy.protocols.RestXmlFactory
import software.amazon.smithy.rust.codegen.smithy.traits.S3UnwrappedXmlOutputTrait
import software.amazon.smithy.rustsdk.AwsRuntimeType

/**
Expand All @@ -32,6 +37,7 @@ import software.amazon.smithy.rustsdk.AwsRuntimeType
class S3Decorator : RustCodegenDecorator {
override val name: String = "S3ExtendedError"
override val order: Byte = 0

private fun applies(serviceId: ShapeId) =
serviceId == ShapeId.from("com.amazonaws.s3#AmazonS3")

Expand All @@ -53,6 +59,20 @@ class S3Decorator : RustCodegenDecorator {
it + S3PubUse()
}
}

override fun transformModel(service: ServiceShape, model: Model): Model {
return model.letIf(applies(service.id)) {
ModelTransformer.create().mapShapes(model) { shape ->
// Apply the S3UnwrappedXmlOutput customization to GetBucketLocation (more
// details on the S3UnwrappedXmlOutputTrait)
if (shape is StructureShape && shape.id == ShapeId.from("com.amazonaws.s3#GetBucketLocationOutput")) {
shape.toBuilder().addTrait(S3UnwrappedXmlOutputTrait()).build()
} else {
shape
}
}
}
}
}

class S3(protocolConfig: ProtocolConfig) : RestXml(protocolConfig) {
Expand All @@ -78,7 +98,7 @@ class S3(protocolConfig: ProtocolConfig) : RestXml(protocolConfig) {
let base_err = #{base_errors}::parse_generic_error(response.body().as_ref())?;
Ok(#{s3_errors}::parse_extended_error(base_err, &response))
}
""",
""",
"base_errors" to restXmlErrors,
"s3_errors" to AwsRuntimeType.S3Errors,
"Error" to RuntimeType.GenericError(runtimeConfig)
Expand Down
13 changes: 13 additions & 0 deletions aws/sdk/aws-models/s3-tests.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,16 @@ apply NotFound @httpResponseTests([
}
}
])

apply GetBucketLocation @httpResponseTests([
{
id: "GetBucketLocation",
documentation: "This test case validates https://github.com/awslabs/aws-sdk-rust/issues/116",
code: 200,
body: "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<LocationConstraint xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">us-west-2</LocationConstraint>",
params: {
"LocationConstraint": "us-west-2"
},
protocol: "aws.protocols#restXml"
}
])
4 changes: 0 additions & 4 deletions aws/sdk/integration-tests/s3/src/lib.rs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import software.amazon.smithy.rust.codegen.smithy.isOptional
import software.amazon.smithy.rust.codegen.smithy.protocols.XmlMemberIndex
import software.amazon.smithy.rust.codegen.smithy.protocols.XmlNameIndex
import software.amazon.smithy.rust.codegen.smithy.protocols.deserializeFunctionName
import software.amazon.smithy.rust.codegen.smithy.traits.S3UnwrappedXmlOutputTrait
import software.amazon.smithy.rust.codegen.util.dq
import software.amazon.smithy.rust.codegen.util.expectMember
import software.amazon.smithy.rust.codegen.util.hasTrait
Expand Down Expand Up @@ -191,8 +192,12 @@ class XmlBindingTraitParserGenerator(
*codegenScope
)
val context = OperationWrapperContext(operationShape, shapeName, xmlError)
writeOperationWrapper(context) { tagName ->
parseStructureInner(members, builder = "builder", Ctx(tag = tagName, accum = null))
if (outputShape.hasTrait<S3UnwrappedXmlOutputTrait>()) {
unwrappedResponseParser("builder", "decoder", "start_el", outputShape.members())
} else {
writeOperationWrapper(context) { tagName ->
parseStructureInner(members, builder = "builder", Ctx(tag = tagName, accum = null))
}
}
rust("Ok(builder)")
}
Expand Down Expand Up @@ -233,6 +238,31 @@ class XmlBindingTraitParserGenerator(
TODO("Document shapes are not supported by rest XML")
}

private fun RustWriter.unwrappedResponseParser(
builder: String,
decoder: String,
element: String,
members: Collection<MemberShape>
) {
check(members.size == 1) {
"The S3UnwrappedXmlOutputTrait is only allowed on structs with exactly one member"
}
val member = members.first()
rustBlock("match $element") {
case(member) {
val temp = safeName()
withBlock("let $temp =", ";") {
parseMember(
member,
Ctx(tag = decoder, accum = "$builder.${symbolProvider.toMemberName(member)}.take()")
)
}
rust("$builder = $builder.${member.setterName()}($temp);")
}
rustTemplate("_ => return Err(#{XmlError}::custom(\"expected ${member.xmlName()} tag\"))", *codegenScope)
}
}

/**
* Update a structure builder based on the [members], specifying where to find each member (document vs. attributes)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/

package software.amazon.smithy.rust.codegen.smithy.traits

import software.amazon.smithy.model.node.Node
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.traits.AnnotationTrait

/**
* S3's GetBucketLocation response shape can't be represented with Smithy's restXml protocol
* without customization. We add this trait to the S3 model at codegen time so that a different
* code path is taken in the XML deserialization codegen to generate code that parses the S3
* response shape correctly.
*
* From what the S3 model states, the generated parser would expect:
* ```
* <LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
* <LocationConstraint>us-west-2</LocationConstraint>
* </LocationConstraint>
* ```
*
* But S3 actually responds with:
* ```
* <LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">us-west-2</LocationConstraint>
* ```
*/
class S3UnwrappedXmlOutputTrait : AnnotationTrait(ID, Node.objectNode()) {
companion object {
val ID = ShapeId.from("smithy.api.internal#s3UnwrappedXmlOutputTrait")
}
}

0 comments on commit 8075c77

Please sign in to comment.