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

Customize S3's GetBucketLocation call to correctly parse the response #516

Merged
merged 3 commits into from
Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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())
Copy link
Collaborator

Choose a reason for hiding this comment

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

is there a reason you didn't just call parseStructureInner here? You could still validate the one member constraint here if you wanted

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, parseStructInner calls parseLoop which advances to the next tag (which won't exist in this case).

} 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")
}
}