diff --git a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt
index a4d0c7c8ae..6db5a99be3 100644
--- a/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt
+++ b/aws/sdk-codegen/src/main/kotlin/software/amazon/smithy/rustsdk/customize/s3/S3Decorator.kt
@@ -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
@@ -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
/**
@@ -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")
@@ -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) {
@@ -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)
diff --git a/aws/sdk/aws-models/s3-tests.smithy b/aws/sdk/aws-models/s3-tests.smithy
index 3cca54df42..e20dbde56d 100644
--- a/aws/sdk/aws-models/s3-tests.smithy
+++ b/aws/sdk/aws-models/s3-tests.smithy
@@ -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: "\nus-west-2",
+ params: {
+ "LocationConstraint": "us-west-2"
+ },
+ protocol: "aws.protocols#restXml"
+ }
+])
diff --git a/aws/sdk/integration-tests/s3/src/lib.rs b/aws/sdk/integration-tests/s3/src/lib.rs
deleted file mode 100644
index c6d888f029..0000000000
--- a/aws/sdk/integration-tests/s3/src/lib.rs
+++ /dev/null
@@ -1,4 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0.
- */
diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt
index 572a4a4b4f..29330bd5e3 100644
--- a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt
+++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/protocols/parse/XmlBindingTraitParserGenerator.kt
@@ -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
@@ -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()) {
+ unwrappedResponseParser("builder", "decoder", "start_el", outputShape.members())
+ } else {
+ writeOperationWrapper(context) { tagName ->
+ parseStructureInner(members, builder = "builder", Ctx(tag = tagName, accum = null))
+ }
}
rust("Ok(builder)")
}
@@ -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
+ ) {
+ 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)
*/
diff --git a/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/S3UnwrappedXmlOutputTrait.kt b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/S3UnwrappedXmlOutputTrait.kt
new file mode 100644
index 0000000000..7d7eb41b25
--- /dev/null
+++ b/codegen/src/main/kotlin/software/amazon/smithy/rust/codegen/smithy/traits/S3UnwrappedXmlOutputTrait.kt
@@ -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:
+ * ```
+ *
+ * us-west-2
+ *
+ * ```
+ *
+ * But S3 actually responds with:
+ * ```
+ * us-west-2
+ * ```
+ */
+class S3UnwrappedXmlOutputTrait : AnnotationTrait(ID, Node.objectNode()) {
+ companion object {
+ val ID = ShapeId.from("smithy.api.internal#s3UnwrappedXmlOutputTrait")
+ }
+}