Skip to content

Commit f62b7cf

Browse files
authored
fix: s3 custom treatment getbucketlocation response (#897)
1 parent 507f673 commit f62b7cf

File tree

8 files changed

+130
-21
lines changed

8 files changed

+130
-21
lines changed

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ object RuntimeTypes {
251251
val XmlError = symbol("XmlError")
252252
val XmlSerializer = symbol("XmlSerializer")
253253
val XmlDeserializer = symbol("XmlDeserializer")
254+
val XmlUnwrappedOutput = symbol("XmlUnwrappedOutput")
254255
}
255256

256257
object SerdeFormUrl : RuntimeTypePackage(KotlinDependency.SERDE_FORM_URL) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.kotlin.codegen.model.traits
6+
7+
import software.amazon.smithy.model.node.Node
8+
import software.amazon.smithy.model.node.ObjectNode
9+
import software.amazon.smithy.model.shapes.ShapeId
10+
import software.amazon.smithy.model.traits.AnnotationTrait
11+
12+
/**
13+
* Indicates the annotated structure is unwrapped XML output.
14+
*
15+
* Refer to this [s3 specific example](https://smithy.io/2.0/aws/customizations/s3-customizations.html#aws-customizations-s3unwrappedxmloutput-trait)
16+
*/
17+
class UnwrappedXmlOutput(node: ObjectNode) : AnnotationTrait(ID, node) {
18+
companion object {
19+
val ID: ShapeId = ShapeId.from("smithy.kotlin.traits#unwrappedXmlOutput")
20+
}
21+
constructor() : this(Node.objectNode())
22+
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGenerator.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import software.amazon.smithy.kotlin.codegen.core.*
1212
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
1313
import software.amazon.smithy.kotlin.codegen.lang.toEscapedLiteral
1414
import software.amazon.smithy.kotlin.codegen.model.*
15-
import software.amazon.smithy.kotlin.codegen.rendering.serde.*
15+
import software.amazon.smithy.kotlin.codegen.rendering.serde.deserializerName
16+
import software.amazon.smithy.kotlin.codegen.rendering.serde.formatInstant
17+
import software.amazon.smithy.kotlin.codegen.rendering.serde.parseInstant
18+
import software.amazon.smithy.kotlin.codegen.rendering.serde.serializerName
1619
import software.amazon.smithy.model.Model
1720
import software.amazon.smithy.model.knowledge.HttpBinding
1821
import software.amazon.smithy.model.shapes.*

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import software.amazon.smithy.kotlin.codegen.core.*
99
import software.amazon.smithy.kotlin.codegen.integration.SectionId
1010
import software.amazon.smithy.kotlin.codegen.integration.SectionKey
1111
import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
12-
import software.amazon.smithy.kotlin.codegen.model.*
12+
import software.amazon.smithy.kotlin.codegen.model.getTrait
13+
import software.amazon.smithy.kotlin.codegen.model.hasStreamingMember
14+
import software.amazon.smithy.kotlin.codegen.model.hasTrait
1315
import software.amazon.smithy.kotlin.codegen.model.knowledge.AuthIndex
16+
import software.amazon.smithy.kotlin.codegen.model.operationSignature
1417
import software.amazon.smithy.kotlin.codegen.rendering.auth.AuthSchemeProviderAdapterGenerator
1518
import software.amazon.smithy.kotlin.codegen.rendering.auth.IdentityProviderConfigGenerator
1619
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointResolverAdapterGenerator
@@ -36,11 +39,6 @@ abstract class HttpProtocolClientGenerator(
3639
val OperationShape: SectionKey<OperationShape> = SectionKey("OperationShape")
3740
}
3841

39-
object OperationDeserializerBinding : SectionId {
40-
// Context for operation being codegened at the time of section invocation
41-
val Operation: SectionKey<OperationShape> = SectionKey("Operation")
42-
}
43-
4442
object OperationTelemetryBuilder : SectionId {
4543
val Operation: SectionKey<OperationShape> = SectionKey("Operation")
4644
}
@@ -204,12 +202,10 @@ abstract class HttpProtocolClientGenerator(
204202
}
205203
}
206204

207-
writer.declareSection(OperationDeserializerBinding, mapOf(OperationDeserializerBinding.Operation to op)) {
208-
if (outputShape.isPresent) {
209-
write("deserializer = ${op.deserializerName()}()")
210-
} else {
211-
write("deserializer = UnitDeserializer")
212-
}
205+
if (outputShape.isPresent) {
206+
writer.write("deserializer = ${op.deserializerName()}()")
207+
} else {
208+
writer.write("deserializer = UnitDeserializer")
213209
}
214210

215211
// execution context

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/XmlSerdeDescriptorGenerator.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@
66
package software.amazon.smithy.kotlin.codegen.rendering.serde
77

88
import software.amazon.smithy.codegen.core.Symbol
9-
import software.amazon.smithy.kotlin.codegen.core.*
10-
import software.amazon.smithy.kotlin.codegen.model.*
9+
import software.amazon.smithy.kotlin.codegen.core.RenderingContext
10+
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
11+
import software.amazon.smithy.kotlin.codegen.core.addImport
12+
import software.amazon.smithy.kotlin.codegen.core.defaultName
13+
import software.amazon.smithy.kotlin.codegen.model.expectShape
14+
import software.amazon.smithy.kotlin.codegen.model.expectTrait
15+
import software.amazon.smithy.kotlin.codegen.model.getTrait
16+
import software.amazon.smithy.kotlin.codegen.model.hasTrait
1117
import software.amazon.smithy.kotlin.codegen.model.traits.SyntheticClone
18+
import software.amazon.smithy.kotlin.codegen.model.traits.UnwrappedXmlOutput
1219
import software.amazon.smithy.kotlin.codegen.utils.dq
1320
import software.amazon.smithy.model.shapes.*
1421
import software.amazon.smithy.model.traits.*
@@ -50,6 +57,10 @@ open class XmlSerdeDescriptorGenerator(
5057
objTraits.add(serdeTrait)
5158
}
5259

60+
if (objectShape.hasTrait<UnwrappedXmlOutput>()) {
61+
objTraits.add(SerdeXml.XmlUnwrappedOutput)
62+
}
63+
5364
return objTraits
5465
}
5566

runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlDeserializer.kt

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import aws.smithy.kotlin.runtime.content.BigInteger
1111
import aws.smithy.kotlin.runtime.content.Document
1212
import aws.smithy.kotlin.runtime.serde.*
1313

14+
private const val FIRST_FIELD_INDEX: Int = 0
15+
1416
// Represents aspects of SdkFieldDescriptor that are particular to the Xml format
1517
internal sealed class FieldLocation {
1618
// specifies the mapping to a sdk field index
@@ -69,7 +71,8 @@ public class XmlDeserializer(
6971
throw DeserializationException("Expected last parsed token to be ${XmlToken.BeginElement::class} but was ${reader.lastToken}")
7072
}
7173

72-
return XmlStructDeserializer(descriptor, reader.subTreeReader(), parentToken, attribFields)
74+
val unwrapped = descriptor.hasTrait<XmlUnwrappedOutput>()
75+
return XmlStructDeserializer(descriptor, reader.subTreeReader(XmlStreamReader.SubtreeStartDepth.CURRENT), parentToken, attribFields, unwrapped)
7376
}
7477

7578
override fun deserializeList(descriptor: SdkFieldDescriptor): Deserializer.ElementIterator {
@@ -213,16 +216,22 @@ internal class XmlListDeserializer(
213216
* @param parentToken initial token of associated structure
214217
* @param parsedFieldLocations list of [FieldLocation] representing values able to be loaded into deserialized instances
215218
*/
216-
internal class XmlStructDeserializer(
219+
private class XmlStructDeserializer(
217220
private val objDescriptor: SdkObjectDescriptor,
218-
private val reader: XmlStreamReader,
221+
reader: XmlStreamReader,
219222
private val parentToken: XmlToken.BeginElement,
220223
private val parsedFieldLocations: MutableList<FieldLocation> = mutableListOf(),
224+
private val unwrapped: Boolean,
221225
) : Deserializer.FieldIterator {
222226
// Used to track direct deserialization or further nesting between calls to findNextFieldIndex() and deserialize<Type>()
223227
private var reentryFlag: Boolean = false
224228

229+
private val reader: XmlStreamReader = if (unwrapped) reader else reader.subTreeReader(XmlStreamReader.SubtreeStartDepth.CHILD)
230+
225231
override fun findNextFieldIndex(): Int? {
232+
if (unwrapped) {
233+
return if (reader.peek() is XmlToken.Text) FIRST_FIELD_INDEX else null
234+
}
226235
if (inNestedMode()) {
227236
// Returning from a nested struct call. Nested deserializer consumed
228237
// tokens so clear them here to avoid processing stale state
@@ -251,6 +260,10 @@ internal class XmlStructDeserializer(
251260
}
252261

253262
private fun <T> deserializeValue(transform: ((String) -> T)): T {
263+
if (unwrapped) {
264+
val value = reader.takeNextAs<XmlToken.Text>().value ?: ""
265+
return transform(value)
266+
}
254267
// Set and validate mode
255268
reentryFlag = false
256269
if (parsedFieldLocations.isEmpty()) throw DeserializationException("matchedFields is empty, was findNextFieldIndex() called?")
@@ -383,16 +396,20 @@ private fun SdkObjectDescriptor.fieldTokenMatcher(fieldDescriptor: SdkFieldDescr
383396
return fieldDescriptor.nameMatches(beginElement.name.tag)
384397
}
385398

386-
// Return the next token of the specified type or throw [DeserializerStateException] if incorrect type.
399+
/**
400+
* Return the next token of the specified type or throw [DeserializationException] if incorrect type.
401+
*/
387402
internal inline fun <reified TExpected : XmlToken> XmlStreamReader.takeNextAs(): TExpected {
388403
val token = this.nextToken() ?: throw DeserializationException("Expected ${TExpected::class} but instead found null")
389404
requireToken<TExpected>(token)
390405
return token as TExpected
391406
}
392407

393-
// require that the given token be of type [TExpected] or else throw an exception
408+
/**
409+
* Require that the given token be of type [TExpected] or else throw an exception
410+
*/
394411
internal inline fun <reified TExpected> requireToken(token: XmlToken) {
395412
if (token::class != TExpected::class) {
396-
throw DeserializationException("expected ${TExpected::class}; found ${token::class} ($token)")
413+
throw DeserializationException("Expected ${TExpected::class}; found ${token::class} ($token)")
397414
}
398415
}

runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlFieldTraits.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ public data class XmlCollectionName(
6767
@InternalApi
6868
public object Flattened : FieldTrait
6969

70+
/**
71+
* Specifies that an object is XML unwrapped response.
72+
*
73+
* Refer to: [s3 specific example](https://smithy.io/2.0/aws/customizations/s3-customizations.html#aws-customizations-s3unwrappedxmloutput-trait)
74+
*/
75+
@InternalApi
76+
public object XmlUnwrappedOutput : FieldTrait
77+
7078
/**
7179
* Denotes a structure that represents an error. There are special rules for error deserialization
7280
* in various XML-based protocols. This trait provides necessary context to the deserializer to properly

runtime/serde/serde-xml/common/test/aws/smithy/kotlin/runtime/serde/xml/XmlDeserializerStructTest.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,57 @@ class XmlDeserializerStructTest {
200200
println(bst.nested?.nested)
201201
}
202202

203+
class BasicUnwrappedStructTest {
204+
var x: String? = null
205+
206+
companion object {
207+
val X_DESCRIPTOR = SdkFieldDescriptor(SerialKind.String, XmlSerialName("x"))
208+
private val OBJ_DESCRIPTOR = SdkObjectDescriptor.build {
209+
trait(XmlSerialName("payload"))
210+
trait(XmlUnwrappedOutput)
211+
field(X_DESCRIPTOR)
212+
}
213+
214+
fun deserialize(deserializer: Deserializer): BasicUnwrappedStructTest {
215+
val result = BasicUnwrappedStructTest()
216+
deserializer.deserializeStruct(OBJ_DESCRIPTOR) {
217+
loop@ while (true) {
218+
when (findNextFieldIndex()) {
219+
X_DESCRIPTOR.index -> result.x = deserializeString()
220+
null -> break@loop
221+
else -> throw DeserializationException(IllegalStateException("unexpected field in BasicUnwrappedStructTest deserializer"))
222+
}
223+
}
224+
}
225+
return result
226+
}
227+
}
228+
}
229+
230+
@Test
231+
fun itHandlesBasicUnwrappedStructs() {
232+
val payload = """
233+
<x>text</x>
234+
""".encodeToByteArray()
235+
236+
val deserializer = XmlDeserializer(payload)
237+
val bst = BasicUnwrappedStructTest.deserialize(deserializer)
238+
239+
assertEquals("text", bst.x)
240+
}
241+
242+
@Test
243+
fun itHandlesBasicUnwrappedStructsWithNullValues() {
244+
val payload = """
245+
<x></x>
246+
""".encodeToByteArray()
247+
248+
val deserializer = XmlDeserializer(payload)
249+
val bst = BasicUnwrappedStructTest.deserialize(deserializer)
250+
251+
assertEquals(null, bst.x)
252+
}
253+
203254
class AliasStruct {
204255
var message: String? = null
205256
var attribute: String? = null

0 commit comments

Comments
 (0)