Skip to content

Commit 422e04e

Browse files
authored
fix: correctly deserialize consecutive XML flat maps (#879)
1 parent afb36c8 commit 422e04e

File tree

4 files changed

+266
-5
lines changed

4 files changed

+266
-5
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "a2c5037e-db65-45f5-8033-acbd3bf241ee",
3+
"type": "bugfix",
4+
"description": "Properly deserialize XML flat maps",
5+
"issues": [
6+
"https://github.com/awslabs/aws-sdk-kotlin/issues/962"
7+
]
8+
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,14 @@ public class XmlDeserializer(
7979
return XmlListDeserializer(reader.subTreeReader(depth), descriptor)
8080
}
8181

82-
override fun deserializeMap(descriptor: SdkFieldDescriptor): Deserializer.EntryIterator =
83-
XmlMapDeserializer(reader.subTreeReader(XmlStreamReader.SubtreeStartDepth.CURRENT), descriptor)
82+
override fun deserializeMap(descriptor: SdkFieldDescriptor): Deserializer.EntryIterator {
83+
val depth = when (descriptor.hasTrait<Flattened>()) {
84+
true -> XmlStreamReader.SubtreeStartDepth.CURRENT
85+
else -> XmlStreamReader.SubtreeStartDepth.CHILD
86+
}
87+
88+
return XmlMapDeserializer(reader.subTreeReader(depth), descriptor)
89+
}
8490
}
8591

8692
/**
@@ -98,10 +104,15 @@ internal class XmlMapDeserializer(
98104
private val mapTrait = descriptor.findTrait<XmlMapName>() ?: XmlMapName.Default
99105

100106
override fun hasNextEntry(): Boolean {
101-
// Seek to either the entry or key token depending on the flatness of the map
107+
val compareTo = when (descriptor.hasTrait<Flattened>()) {
108+
true -> descriptor.findTrait<XmlSerialName>()?.name ?: mapTrait.key // Prefer seeking to XmlSerialName if the trait exists
109+
false -> mapTrait.entry
110+
}
111+
112+
// Seek to either the XML serial name, entry, or key token depending on the flatness of the map and if the name trait is present
102113
val nextEntryToken = when (descriptor.hasTrait<Flattened>()) {
103-
true -> reader.seek<XmlToken.BeginElement> { it.name.local == mapTrait.key }
104-
false -> reader.seek<XmlToken.BeginElement> { it.name.local == mapTrait.entry }
114+
true -> reader.peekSeek<XmlToken.BeginElement> { it.name.local == compareTo }
115+
false -> reader.seek<XmlToken.BeginElement> { it.name.local == compareTo }
105116
}
106117

107118
return nextEntryToken != null

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,33 @@ public inline fun <reified T : XmlToken> XmlStreamReader.seek(selectionPredicate
7878
return token as T?
7979
}
8080

81+
/**
82+
* Peek and seek forward until a token of type [T] is found.
83+
* If it matches the [selectionPredicate], consume the token and return it. Otherwise, return `null` without consuming the token.
84+
*
85+
* @param selectionPredicate predicate that evaluates nodes of the required type to match
86+
*/
87+
public inline fun <reified T : XmlToken> XmlStreamReader.peekSeek(selectionPredicate: (T) -> Boolean = { true }): T? {
88+
var token: XmlToken? = lastToken
89+
90+
if (token != null && token is T) {
91+
return if (selectionPredicate.invoke(token)) token else null
92+
}
93+
94+
do {
95+
if (token is T) {
96+
return if (selectionPredicate.invoke(token)) {
97+
nextToken() as T
98+
} else {
99+
null
100+
}
101+
} else { nextToken() }
102+
token = peek()
103+
} while (token != null)
104+
105+
return null
106+
}
107+
81108
/**
82109
* Creates an [XmlStreamReader] instance
83110
*/

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

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,221 @@ class XmlDeserializerMapTest {
404404

405405
println(resp)
406406
}
407+
408+
// https://github.com/awslabs/aws-sdk-kotlin/issues/962
409+
@Test
410+
fun itHandlesConsecutiveFlatMaps() {
411+
val payload = """
412+
<object>
413+
<firstMap>
414+
<key>key1</key>
415+
<value>1</value>
416+
</firstMap>
417+
<firstMap>
418+
<key>key2</key>
419+
<value>2</value>
420+
</firstMap>
421+
<firstMap>
422+
<key>key3</key>
423+
<value>3</value>
424+
</firstMap>
425+
<secondMap>
426+
<key>key4</key>
427+
<value>4</value>
428+
</secondMap>
429+
<secondMap>
430+
<key>key5</key>
431+
<value>5</value>
432+
</secondMap>
433+
</object>
434+
""".encodeToByteArray()
435+
val firstMapDescriptor = SdkFieldDescriptor(SerialKind.Map, XmlSerialName("firstMap"), XmlMapName(null, "key", "value"), Flattened)
436+
val secondMapDescriptor = SdkFieldDescriptor(SerialKind.Map, XmlSerialName("secondMap"), XmlMapName(null, "key", "value"), Flattened)
437+
438+
val objDescriptor = SdkObjectDescriptor.build {
439+
trait(XmlSerialName("object"))
440+
field(firstMapDescriptor)
441+
field(secondMapDescriptor)
442+
}
443+
var firstMap = mutableMapOf<String, Int>()
444+
var secondMap = mutableMapOf<String, Int>()
445+
val deserializer = XmlDeserializer(payload)
446+
deserializer.deserializeStruct(objDescriptor) {
447+
loop@while (true) {
448+
when (findNextFieldIndex()) {
449+
firstMapDescriptor.index ->
450+
firstMap =
451+
deserializer.deserializeMap(firstMapDescriptor) {
452+
val map0 = mutableMapOf<String, Int>()
453+
while (hasNextEntry()) {
454+
val k0 = key()
455+
val v0 = if (nextHasValue()) { deserializeInt() } else { deserializeNull(); continue }
456+
map0[k0] = v0
457+
}
458+
map0
459+
}
460+
secondMapDescriptor.index ->
461+
secondMap =
462+
deserializer.deserializeMap(secondMapDescriptor) {
463+
val map0 = mutableMapOf<String, Int>()
464+
while (hasNextEntry()) {
465+
val k0 = key()
466+
val v0 = if (nextHasValue()) { deserializeInt() } else { deserializeNull(); continue }
467+
map0[k0] = v0
468+
}
469+
map0
470+
}
471+
null -> break@loop
472+
else -> skipValue()
473+
}
474+
}
475+
}
476+
477+
val expectedFirstMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3)
478+
firstMap.shouldContainExactly(expectedFirstMap)
479+
val expectedSecondMap = mapOf("key4" to 4, "key5" to 5)
480+
secondMap.shouldContainExactly(expectedSecondMap)
481+
}
482+
483+
@Test
484+
fun itHandlesMapsFollowedByFlatMaps() {
485+
val payload = """
486+
<object>
487+
<map>
488+
<entry>
489+
<key>key1</key>
490+
<value>1</value>
491+
</entry>
492+
<entry>
493+
<key>key2</key>
494+
<value>2</value>
495+
</entry>
496+
</map>
497+
<flatMap>
498+
<key>key3</key>
499+
<value>3</value>
500+
</flatMap>
501+
<flatMap>
502+
<key>key4</key>
503+
<value>4</value>
504+
</flatMap>
505+
</object>
506+
""".encodeToByteArray()
507+
val mapDescriptor = SdkFieldDescriptor(SerialKind.Map, XmlSerialName("map"))
508+
val flatMapDescriptor = SdkFieldDescriptor(SerialKind.Map, XmlSerialName("flatMap"), Flattened)
509+
val objDescriptor = SdkObjectDescriptor.build {
510+
trait(XmlSerialName("object"))
511+
field(mapDescriptor)
512+
field(flatMapDescriptor)
513+
}
514+
515+
var map = mutableMapOf<String, Int>()
516+
var flatMap = mutableMapOf<String, Int>()
517+
518+
val deserializer = XmlDeserializer(payload)
519+
deserializer.deserializeStruct(objDescriptor) {
520+
loop@while (true) {
521+
when (findNextFieldIndex()) {
522+
mapDescriptor.index ->
523+
map =
524+
deserializer.deserializeMap(mapDescriptor) {
525+
val map0 = mutableMapOf<String, Int>()
526+
while (hasNextEntry()) {
527+
val k0 = key()
528+
val v0 = if (nextHasValue()) { deserializeInt() } else { deserializeNull(); continue }
529+
map0[k0] = v0
530+
}
531+
map0
532+
}
533+
flatMapDescriptor.index ->
534+
flatMap =
535+
deserializer.deserializeMap(flatMapDescriptor) {
536+
val map0 = mutableMapOf<String, Int>()
537+
while (hasNextEntry()) {
538+
val k0 = key()
539+
val v0 = if (nextHasValue()) { deserializeInt() } else { deserializeNull(); continue }
540+
map0[k0] = v0
541+
}
542+
map0
543+
}
544+
null -> break@loop
545+
else -> skipValue()
546+
}
547+
}
548+
}
549+
map.shouldContainExactly(mapOf("key1" to 1, "key2" to 2))
550+
flatMap.shouldContainExactly(mapOf("key3" to 3, "key4" to 4))
551+
}
552+
553+
@Test
554+
fun itHandlesFlatMapsFollowedByMaps() {
555+
val payload = """
556+
<object>
557+
<flatMap>
558+
<key>key3</key>
559+
<value>3</value>
560+
</flatMap>
561+
<flatMap>
562+
<key>key4</key>
563+
<value>4</value>
564+
</flatMap>
565+
<map>
566+
<entry>
567+
<key>key1</key>
568+
<value>1</value>
569+
</entry>
570+
<entry>
571+
<key>key2</key>
572+
<value>2</value>
573+
</entry>
574+
</map>
575+
</object>
576+
""".encodeToByteArray()
577+
val mapDescriptor = SdkFieldDescriptor(SerialKind.Map, XmlSerialName("map"))
578+
val flatMapDescriptor = SdkFieldDescriptor(SerialKind.Map, XmlSerialName("flatMap"), Flattened)
579+
val objDescriptor = SdkObjectDescriptor.build {
580+
trait(XmlSerialName("object"))
581+
field(mapDescriptor)
582+
field(flatMapDescriptor)
583+
}
584+
585+
var map = mutableMapOf<String, Int>()
586+
var flatMap = mutableMapOf<String, Int>()
587+
588+
val deserializer = XmlDeserializer(payload)
589+
deserializer.deserializeStruct(objDescriptor) {
590+
loop@while (true) {
591+
when (findNextFieldIndex()) {
592+
mapDescriptor.index ->
593+
map =
594+
deserializer.deserializeMap(mapDescriptor) {
595+
val map0 = mutableMapOf<String, Int>()
596+
while (hasNextEntry()) {
597+
val k0 = key()
598+
val v0 = if (nextHasValue()) { deserializeInt() } else { deserializeNull(); continue }
599+
map0[k0] = v0
600+
}
601+
map0
602+
}
603+
flatMapDescriptor.index ->
604+
flatMap =
605+
deserializer.deserializeMap(flatMapDescriptor) {
606+
val map0 = mutableMapOf<String, Int>()
607+
while (hasNextEntry()) {
608+
val k0 = key()
609+
val v0 = if (nextHasValue()) { deserializeInt() } else { deserializeNull(); continue }
610+
map0[k0] = v0
611+
}
612+
map0
613+
}
614+
null -> break@loop
615+
else -> skipValue()
616+
}
617+
}
618+
}
619+
map.shouldContainExactly(mapOf("key1" to 1, "key2" to 2))
620+
flatMap.shouldContainExactly(mapOf("key3" to 3, "key4" to 4))
621+
}
407622
}
408623

409624
internal class XmlMapsOperationDeserializer() {

0 commit comments

Comments
 (0)