Skip to content

Commit 43a0baf

Browse files
authored
Add support for the aws.query protocol (#741)
This commit adds support for the `aws.query` protocol, building on top of the `HttpRpcProtocolGenerator` and `Xml[Member|Shape]DeserVisitor` for document serde. Implementations of the `Document[Member|Shape]SerVisitor` have been created that handle form-urlencoded bodies, respecting Smithy's `@xmlName` trait. Assorted minor updates for abstraction and in coordination with updates to the base class have also been made.
1 parent 9880dd9 commit 43a0baf

File tree

9 files changed

+466
-22
lines changed

9 files changed

+466
-22
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ 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(), new AwsRestXml());
30+
return ListUtils.of(new AwsRestJson1_1(), new AwsJsonRpc1_0(), new AwsJsonRpc1_1(),
31+
new AwsRestXml(), new AwsQuery());
3132
}
3233
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,25 @@ static void generateJsonParseBody(GenerationContext context) {
101101
writer.write("");
102102
}
103103

104+
static void generateXmlParseBody(GenerationContext context) {
105+
TypeScriptWriter writer = context.getWriter();
106+
107+
// Include an XML body parser used to deserialize documents from HTTP responses.
108+
writer.addImport("SerdeContext", "__SerdeContext", "@aws-sdk/types");
109+
writer.addDependency(AwsDependency.XML_PARSER);
110+
writer.addDependency(AwsDependency.XML_PARSER_TYPES);
111+
writer.addImport("parse", "pixlParse", "pixl-xml");
112+
writer.openBlock("const parseBody = (streamBody: any, context: __SerdeContext): any => {", "};", () -> {
113+
writer.openBlock("return collectBodyString(streamBody, context).then(encoded => {", "});", () -> {
114+
writer.openBlock("if (encoded.length) {", "}", () -> {
115+
writer.write("return pixlParse(encoded);");
116+
});
117+
writer.write("return {};");
118+
});
119+
});
120+
writer.write("");
121+
}
122+
104123
/**
105124
* Writes an attribute containing information about a Shape's optionally specified
106125
* XML namespace configuration to an attribute of the passed node name.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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.Set;
19+
import software.amazon.smithy.codegen.core.SymbolReference;
20+
import software.amazon.smithy.model.shapes.OperationShape;
21+
import software.amazon.smithy.model.shapes.Shape;
22+
import software.amazon.smithy.model.shapes.StructureShape;
23+
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
24+
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
25+
import software.amazon.smithy.typescript.codegen.integration.HttpRpcProtocolGenerator;
26+
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator;
27+
28+
/**
29+
* Handles generating the aws.query protocol for services. It handles reading and
30+
* writing from document bodies, including generating any functions needed for
31+
* performing serde.
32+
*
33+
* This builds on the foundations of the {@link HttpRpcProtocolGenerator} to handle
34+
* standard components of the HTTP requests and responses.
35+
*
36+
* A set of service-specific customizations exist for Amazon EC2:
37+
*
38+
* @see QueryShapeSerVisitor
39+
* @see XmlShapeDeserVisitor
40+
* @see QueryMemberSerVisitor
41+
* @see XmlMemberDeserVisitor
42+
* @see AwsProtocolUtils
43+
* @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>
45+
*/
46+
final class AwsQuery extends HttpRpcProtocolGenerator {
47+
48+
AwsQuery() {
49+
// AWS Query protocols will attempt to parse error codes from response bodies.
50+
super(true);
51+
}
52+
53+
@Override
54+
protected String getOperationPath(GenerationContext context, OperationShape operationShape) {
55+
return "/";
56+
}
57+
58+
@Override
59+
public String getName() {
60+
return "aws.query";
61+
}
62+
63+
@Override
64+
protected String getDocumentContentType() {
65+
return "application/x-www-form-urlencoded";
66+
}
67+
68+
@Override
69+
protected void generateDocumentBodyShapeSerializers(GenerationContext context, Set<Shape> shapes) {
70+
AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new QueryShapeSerVisitor(context));
71+
}
72+
73+
@Override
74+
protected void generateDocumentBodyShapeDeserializers(GenerationContext context, Set<Shape> shapes) {
75+
AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new XmlShapeDeserVisitor(context));
76+
}
77+
78+
@Override
79+
public void generateSharedComponents(ProtocolGenerator.GenerationContext context) {
80+
super.generateSharedComponents(context);
81+
AwsProtocolUtils.generateXmlParseBody(context);
82+
83+
TypeScriptWriter writer = context.getWriter();
84+
85+
// Generate a function that handles the complex rules around deserializing
86+
// an error code from a rest-xml error.
87+
SymbolReference responseType = getApplicationProtocol().getResponseType();
88+
writer.openBlock("const loadQueryErrorCode = (\n"
89+
+ " output: $T,\n"
90+
+ " data: any\n"
91+
+ "): string => {", "};", responseType, () -> {
92+
93+
// Attempt to fetch the error code from the specific location.
94+
writer.openBlock("if (data.Error.Code !== undefined) {", "}", () -> {
95+
writer.write("return data.Error.Code;");
96+
});
97+
98+
// Default a 404 status code to the NotFound code.
99+
writer.openBlock("if (output.statusCode == 404) {", "}", () -> writer.write("return 'NotFound';"));
100+
101+
// Default to an UnknownError code.
102+
writer.write("return 'UnknownError';");
103+
});
104+
writer.write("");
105+
106+
// Write a single function to handle combining a map in to a valid query string.
107+
writer.openBlock("const buildFormUrlencodedString = (entries: any): string => {", "}", () -> {
108+
writer.write("return Object.keys(entries).map(key => key + '=' + entries[key]).join(\"&\");");
109+
});
110+
}
111+
112+
@Override
113+
protected void writeDefaultHeaders(GenerationContext context, OperationShape operation) {
114+
super.writeDefaultHeaders(context, operation);
115+
AwsProtocolUtils.generateUnsignedPayloadSigV4Header(context, operation);
116+
}
117+
118+
@Override
119+
protected void serializeInputDocument(
120+
GenerationContext context,
121+
OperationShape operation,
122+
StructureShape inputStructure
123+
) {
124+
TypeScriptWriter writer = context.getWriter();
125+
126+
// Gather all the explicit input entries.
127+
writer.write("const entries = $L;",
128+
inputStructure.accept(new QueryMemberSerVisitor(context, "input", Format.DATE_TIME)));
129+
130+
// Set the form encoded string.
131+
writer.openBlock("body = buildFormUrlencodedString({", "});", () -> {
132+
writer.write("...entries,");
133+
// Set the protocol required values.
134+
writer.write("Action: $S,", operation.getId().getName());
135+
writer.write("Version: $S,", context.getService().getVersion());
136+
});
137+
}
138+
139+
@Override
140+
protected void writeErrorCodeParser(GenerationContext context) {
141+
TypeScriptWriter writer = context.getWriter();
142+
143+
// Outsource error code parsing since it's complex for this protocol.
144+
writer.write("errorCode = loadQueryErrorCode(output, parsedOutput.body);");
145+
}
146+
147+
@Override
148+
protected void deserializeOutputDocument(
149+
GenerationContext context,
150+
OperationShape operation,
151+
StructureShape outputStructure
152+
) {
153+
TypeScriptWriter writer = context.getWriter();
154+
155+
String dataSource = "data." + operation.getId().getName() + "Result";
156+
writer.write("contents = $L;",
157+
outputStructure.accept(new XmlMemberDeserVisitor(context, dataSource, Format.DATE_TIME)));
158+
}
159+
}

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

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -87,26 +87,11 @@ protected void generateDocumentBodyShapeDeserializers(GenerationContext context,
8787
@Override
8888
public void generateSharedComponents(GenerationContext context) {
8989
super.generateSharedComponents(context);
90+
AwsProtocolUtils.generateXmlParseBody(context);
9091

9192
TypeScriptWriter writer = context.getWriter();
9293
writer.addDependency(AwsDependency.XML_BUILDER);
9394

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-
11095
// Generate a function that handles the complex rules around deserializing
11196
// an error code from a rest-xml error.
11297
SymbolReference responseType = getApplicationProtocol().getResponseType();

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import software.amazon.smithy.typescript.codegen.integration.DocumentMemberDeserVisitor;
2525
import software.amazon.smithy.typescript.codegen.integration.DocumentMemberSerVisitor;
2626
import software.amazon.smithy.typescript.codegen.integration.HttpRpcProtocolGenerator;
27+
import software.amazon.smithy.utils.StringUtils;
2728

2829
/**
2930
* Handles general components across the AWS JSON protocols that have do not have
@@ -45,9 +46,15 @@ abstract class JsonRpcProtocolGenerator extends HttpRpcProtocolGenerator {
4546
* Creates a AWS JSON RPC protocol generator.
4647
*/
4748
JsonRpcProtocolGenerator() {
49+
// AWS JSON RPC protocols will attempt to parse error codes from response bodies.
4850
super(true);
4951
}
5052

53+
@Override
54+
protected String getOperationPath(GenerationContext context, OperationShape operationShape) {
55+
return "/" + StringUtils.capitalize(operationShape.getId().getName());
56+
}
57+
5158
protected Format getDocumentTimestampFormat() {
5259
return Format.EPOCH_SECONDS;
5360
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
* Visitor to generate serialization functions for shapes in AWS JSON protocol
3737
* document bodies.
3838
*
39-
* No standard visitation methods are overridden; function body generation for all
40-
* expected serializers is handled by this class.
39+
* This class handles function body generation for all types expected by the {@code
40+
* DocumentShapeSerVisitor}. No other shape type serialization is overridden.
4141
*
4242
* Timestamps are serialized to {@link Format}.EPOCH_SECONDS by default.
4343
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 software.amazon.smithy.codegen.core.CodegenException;
19+
import software.amazon.smithy.model.shapes.BigDecimalShape;
20+
import software.amazon.smithy.model.shapes.BigIntegerShape;
21+
import software.amazon.smithy.model.shapes.Shape;
22+
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
23+
import software.amazon.smithy.typescript.codegen.integration.DocumentMemberSerVisitor;
24+
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext;
25+
26+
/**
27+
* Overrides the default implementation of BigDecimal and BigInteger shape
28+
* serialization to throw when encountered in the aws.query protocol.
29+
*
30+
* TODO: Work out support for BigDecimal and BigInteger, natively or through a library.
31+
*/
32+
final class QueryMemberSerVisitor extends DocumentMemberSerVisitor {
33+
34+
QueryMemberSerVisitor(GenerationContext context, String dataSource, Format defaultTimestampFormat) {
35+
super(context, dataSource, defaultTimestampFormat);
36+
}
37+
38+
boolean visitSuppliesEntryList(Shape shape) {
39+
return shape.isStructureShape() || shape.isUnionShape()
40+
|| shape.isMapShape() || shape.isListShape() || shape.isSetShape();
41+
}
42+
43+
@Override
44+
public String bigDecimalShape(BigDecimalShape shape) {
45+
// Fail instead of losing precision through Number.
46+
return unsupportedShape(shape);
47+
}
48+
49+
@Override
50+
public String bigIntegerShape(BigIntegerShape shape) {
51+
// Fail instead of losing precision through Number.
52+
return unsupportedShape(shape);
53+
}
54+
55+
private String unsupportedShape(Shape shape) {
56+
throw new CodegenException(String.format("Cannot serialize shape type %s on protocol, shape: %s.",
57+
shape.getType(), shape.getId()));
58+
}
59+
}

0 commit comments

Comments
 (0)