Skip to content

Commit 7f29be8

Browse files
authored
Fix multiple XML collection deserialization bugs (#768)
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 element exists without validating that its parent element existed. This update also bumps the version of smithy-aws-traits to latest.
1 parent 681c241 commit 7f29be8

File tree

4 files changed

+62
-49
lines changed

4 files changed

+62
-49
lines changed

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

+1-1
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

+2-6
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

+7-2
Original file line numberDiff line numberDiff line change
@@ -225,14 +225,19 @@ 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", (dataSource, visitor) -> {
238+
TypeScriptWriter writer = context.getWriter();
239+
writer.write("contents.$L = $L;", memberName, target.accept(visitor));
240+
});
236241
}
237242
}
238243
}

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

+52-40
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
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;
19-
import java.util.function.Supplier;
21+
import java.util.function.BiConsumer;
22+
import java.util.stream.Collectors;
2023
import software.amazon.smithy.codegen.core.CodegenException;
2124
import software.amazon.smithy.model.Model;
2225
import software.amazon.smithy.model.shapes.CollectionShape;
@@ -99,17 +102,35 @@ protected void deserializeStructure(GenerationContext context, StructureShape sh
99102
members.forEach((memberName, memberShape) -> writer.write("$L: undefined,", memberName));
100103
});
101104

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

105113
writer.write("return contents;");
106114
}
107115

108-
void deserializeNamedStructureMember(
116+
/**
117+
* Generates an if statement for deserializing an output member, validating its
118+
* presence at the correct location, handling collections and flattening, and
119+
* dispatching to the supplied function to generate the body of the if statement.
120+
*
121+
* @param context The generation context.
122+
* @param memberName The name of the member being deserialized.
123+
* @param memberShape The shape of the member being deserialized.
124+
* @param inputLocation The parent input location of the member being deserialized.
125+
* @param statementBodyGenerator A function that generates the proper deserialization
126+
* after member presence is validated.
127+
*/
128+
void deserializeNamedMember(
109129
GenerationContext context,
110130
String memberName,
111131
MemberShape memberShape,
112-
Supplier<String> inputLocation
132+
String inputLocation,
133+
BiConsumer<String, DocumentMemberDeserVisitor> statementBodyGenerator
113134
) {
114135
TypeScriptWriter writer = context.getWriter();
115136
Model model = context.getModel();
@@ -122,28 +143,41 @@ void deserializeNamedStructureMember(
122143
Shape target = context.getModel().expectShape(memberShape.getTarget());
123144
// Handle @xmlFlattened for collections and maps.
124145
boolean isFlattened = memberShape.hasTrait(XmlFlattenedTrait.class);
146+
// Handle targets that return multiple entities per member.
147+
boolean deserializationReturnsArray = deserializationReturnsArray(target);
125148

126149
// Build a string based on the traits that represents the location.
127150
// Note we don't need to handle @xmlAttribute here because the parser flattens
128151
// attributes in to the main structure.
129-
StringBuilder sourceBuilder = new StringBuilder(inputLocation.get())
130-
.append("['").append(locationName).append("']");
131-
132-
// Go in to a specialized element for unflattened aggregates
133-
if (deserializationReturnsArray(target) && !isFlattened) {
152+
StringBuilder sourceBuilder = new StringBuilder(inputLocation).append("['").append(locationName).append("']");
153+
// Track any locations we need to validate on our way to the final element.
154+
List<String> locationsToValidate = new ArrayList<>();
155+
156+
// Go in to a specialized element for unflattened aggregates.
157+
if (deserializationReturnsArray && !isFlattened) {
158+
// Make sure we validate the wrapping element is set.
159+
locationsToValidate.add(sourceBuilder.toString());
160+
// Update the target element.
134161
String targetLocation = getUnnamedAggregateTargetLocation(model, target);
135162
sourceBuilder.append("['").append(targetLocation).append("']");
136163
}
137164

138165
// Handle the response property.
139166
String source = sourceBuilder.toString();
140-
writer.openBlock("if ($L !== undefined) {", "}", source, () -> {
141-
if (isFlattened) {
167+
// Validate the resulting target element is set.
168+
locationsToValidate.add(source);
169+
// Build a string of the elements to validate before deserializing.
170+
String validationStatement = locationsToValidate.stream()
171+
.map(location -> location + " !== undefined")
172+
.collect(Collectors.joining(" && "));
173+
writer.openBlock("if ($L) {", "}", validationStatement, () -> {
174+
String dataSource = source;
175+
// The XML parser will set one K:V for a member that could return multiple entries but only has one.
176+
if (deserializationReturnsArray) {
142177
writer.write("const wrappedItem = ($1L instanceof Array) ? $1L : [$1L];", source);
178+
dataSource = "wrappedItem";
143179
}
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)));
180+
statementBodyGenerator.accept(dataSource, getMemberVisitor(dataSource));
147181
});
148182
}
149183

@@ -165,38 +199,16 @@ private String getUnnamedAggregateTargetLocation(Model model, Shape shape) {
165199
@Override
166200
protected void deserializeUnion(GenerationContext context, UnionShape shape) {
167201
TypeScriptWriter writer = context.getWriter();
168-
Model model = context.getModel();
169202

170203
// Check for any known union members and return when we find one.
171204
Map<String, MemberShape> members = shape.getAllMembers();
172205
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);
177206
// 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, () -> {
207+
Shape target = context.getModel().expectShape(memberShape.getTarget());
208+
deserializeNamedMember(context, memberName, memberShape, "output", (dataSource, visitor) -> {
197209
writer.openBlock("return {", "};", () -> {
198210
// Dispatch to the output value provider for any additional handling.
199-
writer.write("$L: $L", memberName, target.accept(getMemberVisitor(source)));
211+
writer.write("$L: $L", memberName, target.accept(visitor));
200212
});
201213
});
202214
});

0 commit comments

Comments
 (0)