Skip to content

Commit c6258d9

Browse files
committed
Fix multiple XML collection deserialization bugs
This commit fixes a couple issues with generating code to deserialize structure, union, or body members that target collection members. 1. Unflattened shapes that could return multiple entries in their collection but only return one would result in an error at runtime trying to iterate over an object like it was an Array. 2. Unflattened shapes that were not set in the provided output would result in an error at runtime when trying to validate the entry exists element without validating that its parent element existed. This update also bumps the version of smithy-aws-traits to latest.
1 parent 133430c commit c6258d9

File tree

4 files changed

+50
-46
lines changed

4 files changed

+50
-46
lines changed

codegen/smithy-aws-typescript-codegen/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ tasks.withType<Test> {
3333
}
3434

3535
dependencies {
36-
api("software.amazon.smithy:smithy-aws-traits:0.9.6")
36+
api("software.amazon.smithy:smithy-aws-traits:0.9.7")
3737
api("software.amazon.smithy:smithy-typescript-codegen:0.1.0")
3838
testCompile("org.junit.jupiter:junit-jupiter-api:5.4.0")
3939
testRuntime("org.junit.jupiter:junit-jupiter-engine:5.4.0")

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsQuery.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
2424
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
2525
import software.amazon.smithy.typescript.codegen.integration.HttpRpcProtocolGenerator;
26-
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator;
2726

2827
/**
2928
* Handles generating the aws.query protocol for services. It handles reading and
@@ -33,15 +32,12 @@
3332
* This builds on the foundations of the {@link HttpRpcProtocolGenerator} to handle
3433
* standard components of the HTTP requests and responses.
3534
*
36-
* A set of service-specific customizations exist for Amazon EC2:
37-
*
3835
* @see QueryShapeSerVisitor
3936
* @see XmlShapeDeserVisitor
4037
* @see QueryMemberSerVisitor
4138
* @see XmlMemberDeserVisitor
4239
* @see AwsProtocolUtils
4340
* @see <a href="https://awslabs.github.io/smithy/spec/xml.html">Smithy XML traits.</a>
44-
* @see <a href="https://awslabs.github.io/smithy/spec/aws-core.html#ec2QueryName-trait">Smithy EC2 Query Name trait.</a>
4541
*/
4642
final class AwsQuery extends HttpRpcProtocolGenerator {
4743

@@ -76,14 +72,14 @@ protected void generateDocumentBodyShapeDeserializers(GenerationContext context,
7672
}
7773

7874
@Override
79-
public void generateSharedComponents(ProtocolGenerator.GenerationContext context) {
75+
public void generateSharedComponents(GenerationContext context) {
8076
super.generateSharedComponents(context);
8177
AwsProtocolUtils.generateXmlParseBody(context);
8278

8379
TypeScriptWriter writer = context.getWriter();
8480

8581
// Generate a function that handles the complex rules around deserializing
86-
// an error code from a rest-xml error.
82+
// an error code from an xml error body.
8783
SymbolReference responseType = getApplicationProtocol().getResponseType();
8884
writer.openBlock("const loadQueryErrorCode = (\n"
8985
+ " output: $T,\n"

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsRestXml.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,14 +225,20 @@ protected void deserializeOutputDocument(
225225
List<HttpBinding> documentBindings
226226
) {
227227
SymbolProvider symbolProvider = context.getSymbolProvider();
228-
XmlShapeDeserVisitor shapeDeserVisitor = new XmlShapeDeserVisitor(context);
228+
XmlShapeDeserVisitor shapeVisitor = new XmlShapeDeserVisitor(context);
229229

230230
for (HttpBinding binding : documentBindings) {
231231
MemberShape memberShape = binding.getMember();
232+
// Grab the target shape so we can use a member deserializer on it.
233+
Shape target = context.getModel().expectShape(memberShape.getTarget());
232234
// The name of the member to get from the output shape.
233235
String memberName = symbolProvider.toMemberName(memberShape);
234236

235-
shapeDeserVisitor.deserializeNamedStructureMember(context, memberName, memberShape, () -> "data");
237+
shapeVisitor.deserializeNamedMember(context, memberName, memberShape, () -> "data",
238+
(dataSource, visitor) -> {
239+
TypeScriptWriter writer = context.getWriter();
240+
writer.write("contents.$L = $L;", memberName, target.accept(visitor));
241+
});
236242
}
237243
}
238244
}

codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlShapeDeserVisitor.java

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@
1515

1616
package software.amazon.smithy.aws.typescript.codegen;
1717

18+
import java.util.ArrayList;
19+
import java.util.List;
1820
import java.util.Map;
21+
import java.util.function.BiConsumer;
1922
import java.util.function.Supplier;
23+
import java.util.stream.Collectors;
2024
import software.amazon.smithy.codegen.core.CodegenException;
2125
import software.amazon.smithy.model.Model;
2226
import software.amazon.smithy.model.shapes.CollectionShape;
@@ -99,17 +103,23 @@ protected void deserializeStructure(GenerationContext context, StructureShape sh
99103
members.forEach((memberName, memberShape) -> writer.write("$L: undefined,", memberName));
100104
});
101105

102-
members.forEach((memberName, memberShape) ->
103-
deserializeNamedStructureMember(context, memberName, memberShape, () -> "output"));
106+
members.forEach((memberName, memberShape) -> {
107+
// Grab the target shape so we can use a member deserializer on it.
108+
Shape target = context.getModel().expectShape(memberShape.getTarget());
109+
deserializeNamedMember(context, memberName, memberShape, () -> "output", (dataSource, visitor) -> {
110+
writer.write("contents.$L = $L;", memberName, target.accept(visitor));
111+
});
112+
});
104113

105114
writer.write("return contents;");
106115
}
107116

108-
void deserializeNamedStructureMember(
117+
void deserializeNamedMember(
109118
GenerationContext context,
110119
String memberName,
111120
MemberShape memberShape,
112-
Supplier<String> inputLocation
121+
Supplier<String> inputLocation,
122+
BiConsumer<String, DocumentMemberDeserVisitor> statementBody
113123
) {
114124
TypeScriptWriter writer = context.getWriter();
115125
Model model = context.getModel();
@@ -122,28 +132,42 @@ void deserializeNamedStructureMember(
122132
Shape target = context.getModel().expectShape(memberShape.getTarget());
123133
// Handle @xmlFlattened for collections and maps.
124134
boolean isFlattened = memberShape.hasTrait(XmlFlattenedTrait.class);
135+
// Handle targets that return multiple entities per member.
136+
boolean deserializationReturnsArray = deserializationReturnsArray(target);
125137

126138
// Build a string based on the traits that represents the location.
127139
// Note we don't need to handle @xmlAttribute here because the parser flattens
128140
// attributes in to the main structure.
129141
StringBuilder sourceBuilder = new StringBuilder(inputLocation.get())
130142
.append("['").append(locationName).append("']");
131-
132-
// Go in to a specialized element for unflattened aggregates
133-
if (deserializationReturnsArray(target) && !isFlattened) {
143+
// Track any locations we need to validate on our way to the final element.
144+
List<String> locationsToValidate = new ArrayList<>();
145+
146+
// Go in to a specialized element for unflattened aggregates.
147+
if (deserializationReturnsArray && !isFlattened) {
148+
// Make sure we validate the wrapping element is set.
149+
locationsToValidate.add(sourceBuilder.toString());
150+
// Update the target element.
134151
String targetLocation = getUnnamedAggregateTargetLocation(model, target);
135152
sourceBuilder.append("['").append(targetLocation).append("']");
136153
}
137154

138155
// Handle the response property.
139156
String source = sourceBuilder.toString();
140-
writer.openBlock("if ($L !== undefined) {", "}", source, () -> {
141-
if (isFlattened) {
157+
// Validate the resulting target element is set.
158+
locationsToValidate.add(source);
159+
// Build a string of the elements to validate before deserializing.
160+
String validationStatement = locationsToValidate.stream()
161+
.map(location -> location + " !== undefined")
162+
.collect(Collectors.joining(" && "));
163+
writer.openBlock("if ($L) {", "}", validationStatement, () -> {
164+
String dataSource = source;
165+
// The XML parser will set one K:V for a member that could return multiple entries but only has one.
166+
if (deserializationReturnsArray) {
142167
writer.write("const wrappedItem = ($1L instanceof Array) ? $1L : [$1L];", source);
168+
dataSource = "wrappedItem";
143169
}
144-
writer.write("contents.$L = $L;", memberName,
145-
// Dispatch to the output value provider for any additional handling.
146-
target.accept(getMemberVisitor(isFlattened ? "wrappedItem" : source)));
170+
statementBody.accept(dataSource, getMemberVisitor(dataSource));
147171
});
148172
}
149173

@@ -165,38 +189,16 @@ private String getUnnamedAggregateTargetLocation(Model model, Shape shape) {
165189
@Override
166190
protected void deserializeUnion(GenerationContext context, UnionShape shape) {
167191
TypeScriptWriter writer = context.getWriter();
168-
Model model = context.getModel();
169192

170193
// Check for any known union members and return when we find one.
171194
Map<String, MemberShape> members = shape.getAllMembers();
172195
members.forEach((memberName, memberShape) -> {
173-
// Use the @xmlName trait if present on the member, otherwise use the member name.
174-
String locationName = memberShape.getTrait(XmlNameTrait.class)
175-
.map(XmlNameTrait::getValue)
176-
.orElse(memberName);
177196
// Grab the target shape so we can use a member deserializer on it.
178-
Shape target = context.getModel()
179-
.expectShape(memberShape.getTarget());
180-
// Handle @xmlFlattened for collections and maps.
181-
boolean isFlattened = memberShape.hasTrait(XmlFlattenedTrait.class);
182-
183-
// Build a string based on the traits that represents the location.
184-
// Note we don't need to handle @xmlAttribute here because the parser flattens
185-
// attributes in to the main structure.
186-
StringBuilder sourceBuilder = new StringBuilder("output['").append(locationName).append("']");
187-
188-
// Go in to a specialized element for unflattened aggregates
189-
if (deserializationReturnsArray(target) && !isFlattened) {
190-
String targetLocation = getUnnamedAggregateTargetLocation(model, target);
191-
sourceBuilder.append("['").append(targetLocation).append("']");
192-
}
193-
194-
// Handle the response property.
195-
String source = sourceBuilder.toString();
196-
writer.openBlock("if ($L !== undefined) {", "}", source, () -> {
197+
Shape target = context.getModel().expectShape(memberShape.getTarget());
198+
deserializeNamedMember(context, memberName, memberShape, () -> "output", (dataSource, visitor) -> {
197199
writer.openBlock("return {", "};", () -> {
198200
// Dispatch to the output value provider for any additional handling.
199-
writer.write("$L: $L", memberName, target.accept(getMemberVisitor(source)));
201+
writer.write("$L: $L", memberName, target.accept(visitor));
200202
});
201203
});
202204
});

0 commit comments

Comments
 (0)