Skip to content

Commit 120c70d

Browse files
authored
feat: Add support for the aws.ec2 protocol (#791)
* Add support for the aws.ec2 protocol This commit adds support for the `aws.ec2` protocol, building on top of the `HttpRpcProtocolGenerator`, `Xml[Member|Shape]DeserVisitor`, and `Query[Member|Shape]SerVisitor` for document serde. The `QueryShapeSerVisitor` has been opened up for re-use by the new `Ec2ShapeSerVisitor` because `aws.ec2` is a specific version of the `aws.query` protocol. Hooks have been made available to influence the specific behavior that is updated. This also fixes a critical bug with query list and map serialization that would have resulted in runtime failues on serilaizing the member target shape's contents. * Update to empty default error code
1 parent 106061b commit 120c70d

File tree

7 files changed

+313
-36
lines changed

7 files changed

+313
-36
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ public class AddProtocols implements TypeScriptIntegration {
2828
@Override
2929
public List<ProtocolGenerator> getProtocolGenerators() {
3030
return ListUtils.of(new AwsRestJson1_1(), new AwsJsonRpc1_0(), new AwsJsonRpc1_1(),
31-
new AwsRestXml(), new AwsQuery());
31+
new AwsRestXml(), new AwsQuery(), new AwsEc2());
3232
}
3333
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
27+
/**
28+
* Handles generating the aws.ec2 protocol for services. It handles reading and
29+
* writing from document bodies, including generating any functions needed for
30+
* performing serde.
31+
*
32+
* This builds on the foundations of the {@link HttpRpcProtocolGenerator} to handle
33+
* standard components of the HTTP requests and responses.
34+
*
35+
* @see Ec2ShapeSerVisitor
36+
* @see XmlShapeDeserVisitor
37+
* @see QueryMemberSerVisitor
38+
* @see XmlMemberDeserVisitor
39+
* @see AwsProtocolUtils
40+
* @see <a href="https://awslabs.github.io/smithy/spec/xml.html">Smithy XML traits.</a>
41+
* @see <a href="https://awslabs.github.io/smithy/spec/aws-core.html#ec2QueryName-trait">Smithy EC2 Query Name trait.</a>
42+
*/
43+
final class AwsEc2 extends HttpRpcProtocolGenerator {
44+
45+
AwsEc2() {
46+
super(true);
47+
}
48+
49+
@Override
50+
protected String getOperationPath(GenerationContext context, OperationShape operationShape) {
51+
return "/";
52+
}
53+
54+
@Override
55+
public String getName() {
56+
return "aws.ec2";
57+
}
58+
59+
@Override
60+
protected String getDocumentContentType() {
61+
return "application/x-www-form-urlencoded";
62+
}
63+
64+
@Override
65+
protected void generateDocumentBodyShapeSerializers(GenerationContext context, Set<Shape> shapes) {
66+
AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new Ec2ShapeSerVisitor(context));
67+
}
68+
69+
@Override
70+
protected void generateDocumentBodyShapeDeserializers(GenerationContext context, Set<Shape> shapes) {
71+
AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new XmlShapeDeserVisitor(context));
72+
}
73+
74+
@Override
75+
public void generateSharedComponents(GenerationContext context) {
76+
super.generateSharedComponents(context);
77+
AwsProtocolUtils.generateXmlParseBody(context);
78+
AwsProtocolUtils.generateBuildFormUrlencodedString(context);
79+
80+
TypeScriptWriter writer = context.getWriter();
81+
82+
// Generate a function that handles the complex rules around deserializing
83+
// an error code from an xml error body.
84+
SymbolReference responseType = getApplicationProtocol().getResponseType();
85+
writer.openBlock("const loadEc2ErrorCode = (\n"
86+
+ " output: $T,\n"
87+
+ " data: any\n"
88+
+ "): string => {", "};", responseType, () -> {
89+
90+
// Attempt to fetch the error code from the specific location, including the wrapper.
91+
writer.openBlock("if (data.Errors.Error.Code !== undefined) {", "}", () -> {
92+
writer.write("return data.Errors.Error.Code;");
93+
});
94+
95+
// Default a 404 status code to the NotFound code.
96+
writer.openBlock("if (output.statusCode == 404) {", "}", () -> writer.write("return 'NotFound';"));
97+
98+
// Default to an empty error code so an unmodeled exception is built.
99+
writer.write("return '';");
100+
});
101+
writer.write("");
102+
}
103+
104+
@Override
105+
protected void writeDefaultHeaders(GenerationContext context, OperationShape operation) {
106+
super.writeDefaultHeaders(context, operation);
107+
AwsProtocolUtils.generateUnsignedPayloadSigV4Header(context, operation);
108+
}
109+
110+
@Override
111+
protected void serializeInputDocument(
112+
GenerationContext context,
113+
OperationShape operation,
114+
StructureShape inputStructure
115+
) {
116+
TypeScriptWriter writer = context.getWriter();
117+
118+
// Gather all the explicit input entries.
119+
writer.write("const entries = $L;",
120+
inputStructure.accept(new QueryMemberSerVisitor(context, "input", Format.DATE_TIME)));
121+
122+
// Set the form encoded string.
123+
writer.openBlock("body = buildFormUrlencodedString({", "});", () -> {
124+
writer.write("...entries,");
125+
// Set the protocol required values.
126+
writer.write("Action: $S,", operation.getId().getName());
127+
writer.write("Version: $S,", context.getService().getVersion());
128+
});
129+
}
130+
131+
@Override
132+
protected void writeErrorCodeParser(GenerationContext context) {
133+
TypeScriptWriter writer = context.getWriter();
134+
135+
// Outsource error code parsing since it's complex for this protocol.
136+
writer.write("errorCode = loadEc2ErrorCode(output, parsedOutput.body);");
137+
}
138+
139+
@Override
140+
protected void deserializeOutputDocument(
141+
GenerationContext context,
142+
OperationShape operation,
143+
StructureShape outputStructure
144+
) {
145+
TypeScriptWriter writer = context.getWriter();
146+
147+
writer.write("contents = $L;",
148+
outputStructure.accept(new XmlMemberDeserVisitor(context, "data", Format.DATE_TIME)));
149+
}
150+
}

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

+17
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,23 @@ static void generateXmlParseBody(GenerationContext context) {
120120
writer.write("");
121121
}
122122

123+
/**
124+
* Writes a form urlencoded string builder function for query based protocols.
125+
* This will escape the keys and values, combine those with an '=', and combine
126+
* those strings with an '&'.
127+
*
128+
* @param context The generation context.
129+
*/
130+
static void generateBuildFormUrlencodedString(GenerationContext context) {
131+
TypeScriptWriter writer = context.getWriter();
132+
133+
// Write a single function to handle combining a map in to a valid query string.
134+
writer.openBlock("const buildFormUrlencodedString = (entries: any): string => {", "}", () -> {
135+
writer.openBlock("return Object.keys(entries).map(", ").join(\"&\");", () ->
136+
writer.write("key => encodeURIComponent(key) + '=' + encodeURIComponent(entries[key])"));
137+
});
138+
}
139+
123140
/**
124141
* Writes an attribute containing information about a Shape's optionally specified
125142
* XML namespace configuration to an attribute of the passed node name.

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

+3-7
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ protected void generateDocumentBodyShapeDeserializers(GenerationContext context,
7575
public void generateSharedComponents(GenerationContext context) {
7676
super.generateSharedComponents(context);
7777
AwsProtocolUtils.generateXmlParseBody(context);
78+
AwsProtocolUtils.generateBuildFormUrlencodedString(context);
7879

7980
TypeScriptWriter writer = context.getWriter();
8081

@@ -94,15 +95,10 @@ public void generateSharedComponents(GenerationContext context) {
9495
// Default a 404 status code to the NotFound code.
9596
writer.openBlock("if (output.statusCode == 404) {", "}", () -> writer.write("return 'NotFound';"));
9697

97-
// Default to an UnknownError code.
98-
writer.write("return 'UnknownError';");
98+
// Default to an empty error code so an unmodeled exception is built.
99+
writer.write("return '';");
99100
});
100101
writer.write("");
101-
102-
// Write a single function to handle combining a map in to a valid query string.
103-
writer.openBlock("const buildFormUrlencodedString = (entries: any): string => {", "}", () -> {
104-
writer.write("return Object.keys(entries).map(key => key + '=' + entries[key]).join(\"&\");");
105-
});
106102
}
107103

108104
@Override

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ public void generateSharedComponents(GenerationContext context) {
116116
// Default a 404 status code to the NotFound code.
117117
writer.openBlock("if (output.statusCode == 404) {", "}", () -> writer.write("return 'NotFound';"));
118118

119-
// Default to an UnknownError code.
120-
writer.write("return 'UnknownError';");
119+
// Default to an empty error code so an unmodeled exception is built.
120+
writer.write("return '';");
121121
});
122122
writer.write("");
123123
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.Optional;
19+
import software.amazon.smithy.aws.traits.Ec2QueryNameTrait;
20+
import software.amazon.smithy.codegen.core.CodegenException;
21+
import software.amazon.smithy.model.shapes.DocumentShape;
22+
import software.amazon.smithy.model.shapes.MemberShape;
23+
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
24+
import software.amazon.smithy.model.traits.XmlNameTrait;
25+
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext;
26+
import software.amazon.smithy.utils.StringUtils;
27+
28+
/**
29+
* Visitor to generate serialization functions for shapes in form-urlencoded
30+
* based document bodies specific to aws.ec2.
31+
*
32+
* This class uses the implementations provided by {@code QueryShapeSerVisitor} but with
33+
* the following protocol specific customizations for aws.ec2:
34+
*
35+
* <ul>
36+
* <li>aws.ec2 flattens all lists, sets, and maps regardless of the {@code @xmlFlattened} trait.</li>
37+
* <li>aws.ec2 respects the {@code @ec2QueryName} trait, then the {@code xmlName}
38+
* trait value with the first letter capitalized.</li>
39+
* </ul>
40+
*
41+
* Timestamps are serialized to {@link Format}.DATE_TIME by default.
42+
*
43+
* @see AwsEc2
44+
* @see QueryShapeSerVisitor
45+
* @see <a href="https://awslabs.github.io/smithy/spec/aws-core.html#ec2QueryName-trait">Smithy EC2 Query Name trait.</a>
46+
*/
47+
final class Ec2ShapeSerVisitor extends QueryShapeSerVisitor {
48+
49+
Ec2ShapeSerVisitor(GenerationContext context) {
50+
super(context);
51+
}
52+
53+
@Override
54+
protected void serializeDocument(GenerationContext context, DocumentShape shape) {
55+
throw new CodegenException(String.format(
56+
"Cannot serialize Document types in the aws.ec2 protocol, shape: %s.", shape.getId()));
57+
}
58+
59+
@Override
60+
protected String getMemberSerializedLocationName(MemberShape memberShape, String defaultValue) {
61+
// The serialization for aws.ec2 prioritizes the @ec2QueryName trait for serialization.
62+
Optional<Ec2QueryNameTrait> trait = memberShape.getTrait(Ec2QueryNameTrait.class);
63+
if (trait.isPresent()) {
64+
return trait.get().getValue();
65+
}
66+
67+
// Fall back to the capitalized @xmlName trait if present on the member,
68+
// otherwise use the capitalized default value.
69+
return memberShape.getTrait(XmlNameTrait.class)
70+
.map(XmlNameTrait::getValue)
71+
.map(StringUtils::capitalize)
72+
.orElseGet(() -> StringUtils.capitalize(defaultValue));
73+
}
74+
75+
@Override
76+
protected boolean isFlattenedMember(MemberShape memberShape) {
77+
// All lists, sets, and maps are flattened in aws.ec2.
78+
return true;
79+
}
80+
}

0 commit comments

Comments
 (0)