Skip to content

Commit d418410

Browse files
authored
Add support for AWS rest-xml protocol (#727)
This commit adds support for the `aws.rest-xml` protocol, building on top of the `HttpBindingProtocolGenerator` for document and payload serde. Implementations of the `DocumentMember[Deser|Ser]Visitor` and the `DocumentShape[Deser|Ser]Visitor` have been created that handle Smithy's XML traits and their influence on protocol serde. A minor update has been made to the `XmlNode` to allow for nodes to be renamed, as the same structure may change XML node names when it is bound to different locations.
1 parent 2b8db92 commit d418410

File tree

10 files changed

+1011
-3
lines changed

10 files changed

+1011
-3
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,6 @@ public class AddProtocols implements TypeScriptIntegration {
2727

2828
@Override
2929
public List<ProtocolGenerator> getProtocolGenerators() {
30-
return ListUtils.of(new AwsRestJson1_1(), new AwsJsonRpc1_0(), new AwsJsonRpc1_1());
30+
return ListUtils.of(new AwsRestJson1_1(), new AwsJsonRpc1_0(), new AwsJsonRpc1_1(), new AwsRestXml());
3131
}
3232
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

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

18+
import static software.amazon.smithy.typescript.codegen.TypeScriptDependency.DEV_DEPENDENCY;
1819
import static software.amazon.smithy.typescript.codegen.TypeScriptDependency.NORMAL_DEPENDENCY;
1920

2021
import java.util.Collections;
@@ -44,7 +45,10 @@ public enum AwsDependency implements SymbolDependencyContainer {
4445
ROUTE53_MIDDLEWARE(NORMAL_DEPENDENCY, "@aws-sdk/middleware-sdk-route53", "^1.0.0-alpha.0"),
4546
BUCKET_ENDPOINT_MIDDLEWARE(NORMAL_DEPENDENCY, "@aws-sdk/middleware-bucket-endpoint", "^1.0.0-alpha.0"),
4647
BODY_CHECKSUM(NORMAL_DEPENDENCY, "@aws-sdk/middleware-apply-body-checksum", "^1.0.0-alpha.0"),
47-
MIDDLEWARE_HOST_HEADER(NORMAL_DEPENDENCY, "@aws-sdk/middleware-host-header", "^1.0.0-alpha.0");
48+
MIDDLEWARE_HOST_HEADER(NORMAL_DEPENDENCY, "@aws-sdk/middleware-host-header", "^1.0.0-alpha.0"),
49+
XML_BUILDER(NORMAL_DEPENDENCY, "@aws-sdk/xml-builder", "^1.0.0-alpha.0"),
50+
XML_PARSER(NORMAL_DEPENDENCY, "pixl-xml", "^1.0.13"),
51+
XML_PARSER_TYPES(DEV_DEPENDENCY, "@types/pixl-xml", "^1.0.1");
4852

4953
public final String packageName;
5054
public final String version;

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

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

18+
import java.util.Optional;
1819
import java.util.Set;
1920
import java.util.TreeSet;
2021
import software.amazon.smithy.aws.traits.UnsignedPayloadTrait;
@@ -23,6 +24,7 @@
2324
import software.amazon.smithy.model.shapes.OperationShape;
2425
import software.amazon.smithy.model.shapes.Shape;
2526
import software.amazon.smithy.model.shapes.ShapeVisitor;
27+
import software.amazon.smithy.model.traits.XmlNamespaceTrait;
2628
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
2729
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext;
2830

@@ -98,4 +100,24 @@ static void generateJsonParseBody(GenerationContext context) {
98100

99101
writer.write("");
100102
}
103+
104+
/**
105+
* Writes an attribute containing information about a Shape's optionally specified
106+
* XML namespace configuration to an attribute of the passed node name.
107+
*
108+
* @param context The generation context.
109+
* @param shape The shape to apply the namespace attribute to, if present on it.
110+
* @param nodeName The node to apply the namespace attribute to.
111+
*/
112+
static void writeXmlNamespace(GenerationContext context, Shape shape, String nodeName) {
113+
shape.getTrait(XmlNamespaceTrait.class).ifPresent(trait -> {
114+
TypeScriptWriter writer = context.getWriter();
115+
String xmlns = "xmlns";
116+
Optional<String> prefix = trait.getPrefix();
117+
if (prefix.isPresent()) {
118+
xmlns += ":" + prefix.get();
119+
}
120+
writer.write("$L.addAttribute($S, $S);", nodeName, xmlns, trait.getUri());
121+
});
122+
}
101123
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
/*
2+
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.aws.typescript.codegen;
17+
18+
import java.util.List;
19+
import java.util.Set;
20+
import software.amazon.smithy.aws.traits.ServiceTrait;
21+
import software.amazon.smithy.codegen.core.SymbolProvider;
22+
import software.amazon.smithy.codegen.core.SymbolReference;
23+
import software.amazon.smithy.model.knowledge.HttpBinding;
24+
import software.amazon.smithy.model.knowledge.HttpBinding.Location;
25+
import software.amazon.smithy.model.shapes.MemberShape;
26+
import software.amazon.smithy.model.shapes.OperationShape;
27+
import software.amazon.smithy.model.shapes.Shape;
28+
import software.amazon.smithy.model.shapes.StructureShape;
29+
import software.amazon.smithy.model.shapes.UnionShape;
30+
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
31+
import software.amazon.smithy.model.traits.XmlNamespaceTrait;
32+
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
33+
import software.amazon.smithy.typescript.codegen.integration.HttpBindingProtocolGenerator;
34+
35+
/**
36+
* Handles generating the aws.rest-xml protocol for services. It handles reading and
37+
* writing from document bodies, including generating any functions needed for
38+
* performing serde.
39+
*
40+
* This builds on the foundations of the {@link HttpBindingProtocolGenerator} to handle
41+
* components of binding to HTTP requests and responses.
42+
*
43+
* A service-specific customization exists for Amazon S3, which doesn't wrap the Error
44+
* object in the response.
45+
*
46+
* TODO: Build an XmlIndex to handle pre-computing resolved values for names, namespaces, and more.
47+
*
48+
* @see XmlShapeSerVisitor
49+
* @see XmlShapeDeserVisitor
50+
* @see XmlMemberSerVisitor
51+
* @see XmlMemberDeserVisitor
52+
* @see AwsProtocolUtils
53+
* @see <a href="https://awslabs.github.io/smithy/spec/http.html">Smithy HTTP protocol bindings.</a>
54+
* @see <a href="https://awslabs.github.io/smithy/spec/xml.html">Smithy XML traits.</a>
55+
*/
56+
final class AwsRestXml extends HttpBindingProtocolGenerator {
57+
58+
AwsRestXml() {
59+
super(true);
60+
}
61+
62+
@Override
63+
protected String getDocumentContentType() {
64+
return "application/xml";
65+
}
66+
67+
@Override
68+
protected Format getDocumentTimestampFormat() {
69+
return Format.DATE_TIME;
70+
}
71+
72+
@Override
73+
public String getName() {
74+
return "aws.rest-xml";
75+
}
76+
77+
@Override
78+
protected void generateDocumentBodyShapeSerializers(GenerationContext context, Set<Shape> shapes) {
79+
AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new XmlShapeSerVisitor(context));
80+
}
81+
82+
@Override
83+
protected void generateDocumentBodyShapeDeserializers(GenerationContext context, Set<Shape> shapes) {
84+
AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new XmlShapeDeserVisitor(context));
85+
}
86+
87+
@Override
88+
public void generateSharedComponents(GenerationContext context) {
89+
super.generateSharedComponents(context);
90+
91+
TypeScriptWriter writer = context.getWriter();
92+
writer.addDependency(AwsDependency.XML_BUILDER);
93+
94+
// Include an XML body parser used to deserialize documents from HTTP responses.
95+
writer.addImport("SerdeContext", "__SerdeContext", "@aws-sdk/types");
96+
writer.addDependency(AwsDependency.XML_PARSER);
97+
writer.addDependency(AwsDependency.XML_PARSER_TYPES);
98+
writer.addImport("parse", "pixlParse", "pixl-xml");
99+
writer.openBlock("const parseBody = (streamBody: any, context: __SerdeContext): any => {", "};", () -> {
100+
writer.openBlock("return collectBodyString(streamBody, context).then(encoded => {", "});", () -> {
101+
writer.openBlock("if (encoded.length) {", "}", () -> {
102+
writer.write("return pixlParse(encoded);");
103+
});
104+
writer.write("return {};");
105+
});
106+
});
107+
108+
writer.write("");
109+
110+
// Generate a function that handles the complex rules around deserializing
111+
// an error code from a rest-xml error.
112+
SymbolReference responseType = getApplicationProtocol().getResponseType();
113+
writer.openBlock("const loadRestXmlErrorCode = (\n"
114+
+ " output: $T,\n"
115+
+ " data: any\n"
116+
+ "): string => {", "};", responseType, () -> {
117+
// Start building the location that contains the error code.
118+
StringBuilder locationBuilder = new StringBuilder("data.");
119+
// Some services, S3 for example, don't wrap the Error object in the response.
120+
if (usesWrappedErrorResponse(context)) {
121+
locationBuilder.append("Error.");
122+
}
123+
locationBuilder.append("Code");
124+
125+
// Attempt to fetch the error code from the specific location.
126+
String errorCodeLocation = locationBuilder.toString();
127+
writer.openBlock("if ($L !== undefined) {", "}", errorCodeLocation, () -> {
128+
writer.write("return $L;", errorCodeLocation);
129+
});
130+
131+
// Default a 404 status code to the NotFound code.
132+
writer.openBlock("if (output.statusCode == 404) {", "}", () -> writer.write("return 'NotFound';"));
133+
134+
// Default to an UnknownError code.
135+
writer.write("return 'UnknownError';");
136+
});
137+
writer.write("");
138+
}
139+
140+
private boolean usesWrappedErrorResponse(GenerationContext context) {
141+
return context.getService().getTrait(ServiceTrait.class)
142+
.map(trait -> !trait.getSdkId().equals("S3"))
143+
.orElse(true);
144+
}
145+
146+
@Override
147+
protected void writeDefaultHeaders(GenerationContext context, OperationShape operation) {
148+
super.writeDefaultHeaders(context, operation);
149+
AwsProtocolUtils.generateUnsignedPayloadSigV4Header(context, operation);
150+
}
151+
152+
@Override
153+
protected void serializeInputDocument(
154+
GenerationContext context,
155+
OperationShape operation,
156+
List<HttpBinding> documentBindings
157+
) {
158+
SymbolProvider symbolProvider = context.getSymbolProvider();
159+
TypeScriptWriter writer = context.getWriter();
160+
161+
// Start with the XML declaration.
162+
writer.write("body = \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\";");
163+
164+
writer.addImport("XmlNode", "__XmlNode", "@aws-sdk/xml-builder");
165+
writer.write("const bodyNode = new __XmlNode($S);", operation.getId().getName());
166+
167+
// Always add @xmlNamespace value of the service to the root node, since we're
168+
// creating a wrapper node not based on a structure.
169+
AwsProtocolUtils.writeXmlNamespace(context, context.getService(), "bodyNode");
170+
171+
XmlShapeSerVisitor shapeSerVisitor = new XmlShapeSerVisitor(context);
172+
173+
for (HttpBinding binding : documentBindings) {
174+
MemberShape memberShape = binding.getMember();
175+
// The name of the member to get from the input shape.
176+
String memberName = symbolProvider.toMemberName(memberShape);
177+
178+
String inputLocation = "input." + memberName;
179+
writer.openBlock("if ($L !== undefined) {", "}", inputLocation, () -> {
180+
shapeSerVisitor.serializeNamedMember(context, memberName, memberShape, () -> inputLocation);
181+
});
182+
}
183+
184+
// Append the generated XML to the body.
185+
writer.write("body += bodyNode.toString();");
186+
}
187+
188+
@Override
189+
protected void serializeInputPayload(
190+
GenerationContext context,
191+
OperationShape operation,
192+
HttpBinding payloadBinding
193+
) {
194+
SymbolProvider symbolProvider = context.getSymbolProvider();
195+
TypeScriptWriter writer = context.getWriter();
196+
197+
MemberShape member = payloadBinding.getMember();
198+
String memberName = symbolProvider.toMemberName(member);
199+
200+
writer.write("let contents: any;");
201+
202+
// Generate an if statement to set the body node if the member is set.
203+
writer.openBlock("if (input.$L !== undefined) {", "}", memberName, () -> {
204+
Shape target = context.getModel().expectShape(member.getTarget());
205+
writer.write("contents = $L;",
206+
getInputValue(context, Location.PAYLOAD, "input." + memberName, member, target));
207+
208+
// Structure and Union payloads will serialize as XML documents via XmlNode.
209+
if (target instanceof StructureShape || target instanceof UnionShape) {
210+
// Start with the XML declaration.
211+
writer.write("body = \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\";");
212+
213+
// Add @xmlNamespace value of the service to the root structure if one doesn't
214+
// exist on the target we're serializing.
215+
if (!target.hasTrait(XmlNamespaceTrait.class)) {
216+
AwsProtocolUtils.writeXmlNamespace(context, context.getService(), "contents");
217+
}
218+
219+
// Append the generated XML to the body.
220+
writer.write("body += contents.toString();");
221+
} else {
222+
// Strings and blobs (streaming or not) will not need any modification.
223+
writer.write("body = contents;");
224+
}
225+
});
226+
}
227+
228+
@Override
229+
protected void writeErrorCodeParser(GenerationContext context) {
230+
TypeScriptWriter writer = context.getWriter();
231+
232+
// Outsource error code parsing since it's complex for this protocol.
233+
writer.write("errorCode = loadRestXmlErrorCode(output, parsedOutput.body);");
234+
}
235+
236+
@Override
237+
protected void deserializeOutputDocument(
238+
GenerationContext context,
239+
Shape operationOrError,
240+
List<HttpBinding> documentBindings
241+
) {
242+
SymbolProvider symbolProvider = context.getSymbolProvider();
243+
XmlShapeDeserVisitor shapeDeserVisitor = new XmlShapeDeserVisitor(context);
244+
245+
for (HttpBinding binding : documentBindings) {
246+
MemberShape memberShape = binding.getMember();
247+
// The name of the member to get from the output shape.
248+
String memberName = symbolProvider.toMemberName(memberShape);
249+
250+
shapeDeserVisitor.deserializeNamedStructureMember(context, memberName, memberShape, () -> "data");
251+
}
252+
}
253+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
/**
2727
* Overrides the default implementation of BigDecimal and BigInteger shape
2828
* serialization to throw when encountered in AWS JSON based protocols.
29+
*
30+
* TODO: Work out support for BigDecimal and BigInteger, natively or through a library.
2931
*/
3032
final class JsonMemberSerVisitor extends DocumentMemberSerVisitor {
3133

0 commit comments

Comments
 (0)