Skip to content

Commit

Permalink
feat: Add support for the aws.ec2 protocol (#791)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
kstich authored Jan 27, 2020
1 parent 106061b commit 120c70d
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ public class AddProtocols implements TypeScriptIntegration {
@Override
public List<ProtocolGenerator> getProtocolGenerators() {
return ListUtils.of(new AwsRestJson1_1(), new AwsJsonRpc1_0(), new AwsJsonRpc1_1(),
new AwsRestXml(), new AwsQuery());
new AwsRestXml(), new AwsQuery(), new AwsEc2());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

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

import java.util.Set;
import software.amazon.smithy.codegen.core.SymbolReference;
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
import software.amazon.smithy.typescript.codegen.integration.HttpRpcProtocolGenerator;

/**
* Handles generating the aws.ec2 protocol for services. It handles reading and
* writing from document bodies, including generating any functions needed for
* performing serde.
*
* This builds on the foundations of the {@link HttpRpcProtocolGenerator} to handle
* standard components of the HTTP requests and responses.
*
* @see Ec2ShapeSerVisitor
* @see XmlShapeDeserVisitor
* @see QueryMemberSerVisitor
* @see XmlMemberDeserVisitor
* @see AwsProtocolUtils
* @see <a href="https://awslabs.github.io/smithy/spec/xml.html">Smithy XML traits.</a>
* @see <a href="https://awslabs.github.io/smithy/spec/aws-core.html#ec2QueryName-trait">Smithy EC2 Query Name trait.</a>
*/
final class AwsEc2 extends HttpRpcProtocolGenerator {

AwsEc2() {
super(true);
}

@Override
protected String getOperationPath(GenerationContext context, OperationShape operationShape) {
return "/";
}

@Override
public String getName() {
return "aws.ec2";
}

@Override
protected String getDocumentContentType() {
return "application/x-www-form-urlencoded";
}

@Override
protected void generateDocumentBodyShapeSerializers(GenerationContext context, Set<Shape> shapes) {
AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new Ec2ShapeSerVisitor(context));
}

@Override
protected void generateDocumentBodyShapeDeserializers(GenerationContext context, Set<Shape> shapes) {
AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new XmlShapeDeserVisitor(context));
}

@Override
public void generateSharedComponents(GenerationContext context) {
super.generateSharedComponents(context);
AwsProtocolUtils.generateXmlParseBody(context);
AwsProtocolUtils.generateBuildFormUrlencodedString(context);

TypeScriptWriter writer = context.getWriter();

// Generate a function that handles the complex rules around deserializing
// an error code from an xml error body.
SymbolReference responseType = getApplicationProtocol().getResponseType();
writer.openBlock("const loadEc2ErrorCode = (\n"
+ " output: $T,\n"
+ " data: any\n"
+ "): string => {", "};", responseType, () -> {

// Attempt to fetch the error code from the specific location, including the wrapper.
writer.openBlock("if (data.Errors.Error.Code !== undefined) {", "}", () -> {
writer.write("return data.Errors.Error.Code;");
});

// Default a 404 status code to the NotFound code.
writer.openBlock("if (output.statusCode == 404) {", "}", () -> writer.write("return 'NotFound';"));

// Default to an empty error code so an unmodeled exception is built.
writer.write("return '';");
});
writer.write("");
}

@Override
protected void writeDefaultHeaders(GenerationContext context, OperationShape operation) {
super.writeDefaultHeaders(context, operation);
AwsProtocolUtils.generateUnsignedPayloadSigV4Header(context, operation);
}

@Override
protected void serializeInputDocument(
GenerationContext context,
OperationShape operation,
StructureShape inputStructure
) {
TypeScriptWriter writer = context.getWriter();

// Gather all the explicit input entries.
writer.write("const entries = $L;",
inputStructure.accept(new QueryMemberSerVisitor(context, "input", Format.DATE_TIME)));

// Set the form encoded string.
writer.openBlock("body = buildFormUrlencodedString({", "});", () -> {
writer.write("...entries,");
// Set the protocol required values.
writer.write("Action: $S,", operation.getId().getName());
writer.write("Version: $S,", context.getService().getVersion());
});
}

@Override
protected void writeErrorCodeParser(GenerationContext context) {
TypeScriptWriter writer = context.getWriter();

// Outsource error code parsing since it's complex for this protocol.
writer.write("errorCode = loadEc2ErrorCode(output, parsedOutput.body);");
}

@Override
protected void deserializeOutputDocument(
GenerationContext context,
OperationShape operation,
StructureShape outputStructure
) {
TypeScriptWriter writer = context.getWriter();

writer.write("contents = $L;",
outputStructure.accept(new XmlMemberDeserVisitor(context, "data", Format.DATE_TIME)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,23 @@ static void generateXmlParseBody(GenerationContext context) {
writer.write("");
}

/**
* Writes a form urlencoded string builder function for query based protocols.
* This will escape the keys and values, combine those with an '=', and combine
* those strings with an '&'.
*
* @param context The generation context.
*/
static void generateBuildFormUrlencodedString(GenerationContext context) {
TypeScriptWriter writer = context.getWriter();

// Write a single function to handle combining a map in to a valid query string.
writer.openBlock("const buildFormUrlencodedString = (entries: any): string => {", "}", () -> {
writer.openBlock("return Object.keys(entries).map(", ").join(\"&\");", () ->
writer.write("key => encodeURIComponent(key) + '=' + encodeURIComponent(entries[key])"));
});
}

/**
* Writes an attribute containing information about a Shape's optionally specified
* XML namespace configuration to an attribute of the passed node name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ protected void generateDocumentBodyShapeDeserializers(GenerationContext context,
public void generateSharedComponents(GenerationContext context) {
super.generateSharedComponents(context);
AwsProtocolUtils.generateXmlParseBody(context);
AwsProtocolUtils.generateBuildFormUrlencodedString(context);

TypeScriptWriter writer = context.getWriter();

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

// Default to an UnknownError code.
writer.write("return 'UnknownError';");
// Default to an empty error code so an unmodeled exception is built.
writer.write("return '';");
});
writer.write("");

// Write a single function to handle combining a map in to a valid query string.
writer.openBlock("const buildFormUrlencodedString = (entries: any): string => {", "}", () -> {
writer.write("return Object.keys(entries).map(key => key + '=' + entries[key]).join(\"&\");");
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ public void generateSharedComponents(GenerationContext context) {
// Default a 404 status code to the NotFound code.
writer.openBlock("if (output.statusCode == 404) {", "}", () -> writer.write("return 'NotFound';"));

// Default to an UnknownError code.
writer.write("return 'UnknownError';");
// Default to an empty error code so an unmodeled exception is built.
writer.write("return '';");
});
writer.write("");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

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

import java.util.Optional;
import software.amazon.smithy.aws.traits.Ec2QueryNameTrait;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.model.shapes.DocumentShape;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
import software.amazon.smithy.model.traits.XmlNameTrait;
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext;
import software.amazon.smithy.utils.StringUtils;

/**
* Visitor to generate serialization functions for shapes in form-urlencoded
* based document bodies specific to aws.ec2.
*
* This class uses the implementations provided by {@code QueryShapeSerVisitor} but with
* the following protocol specific customizations for aws.ec2:
*
* <ul>
* <li>aws.ec2 flattens all lists, sets, and maps regardless of the {@code @xmlFlattened} trait.</li>
* <li>aws.ec2 respects the {@code @ec2QueryName} trait, then the {@code xmlName}
* trait value with the first letter capitalized.</li>
* </ul>
*
* Timestamps are serialized to {@link Format}.DATE_TIME by default.
*
* @see AwsEc2
* @see QueryShapeSerVisitor
* @see <a href="https://awslabs.github.io/smithy/spec/aws-core.html#ec2QueryName-trait">Smithy EC2 Query Name trait.</a>
*/
final class Ec2ShapeSerVisitor extends QueryShapeSerVisitor {

Ec2ShapeSerVisitor(GenerationContext context) {
super(context);
}

@Override
protected void serializeDocument(GenerationContext context, DocumentShape shape) {
throw new CodegenException(String.format(
"Cannot serialize Document types in the aws.ec2 protocol, shape: %s.", shape.getId()));
}

@Override
protected String getMemberSerializedLocationName(MemberShape memberShape, String defaultValue) {
// The serialization for aws.ec2 prioritizes the @ec2QueryName trait for serialization.
Optional<Ec2QueryNameTrait> trait = memberShape.getTrait(Ec2QueryNameTrait.class);
if (trait.isPresent()) {
return trait.get().getValue();
}

// Fall back to the capitalized @xmlName trait if present on the member,
// otherwise use the capitalized default value.
return memberShape.getTrait(XmlNameTrait.class)
.map(XmlNameTrait::getValue)
.map(StringUtils::capitalize)
.orElseGet(() -> StringUtils.capitalize(defaultValue));
}

@Override
protected boolean isFlattenedMember(MemberShape memberShape) {
// All lists, sets, and maps are flattened in aws.ec2.
return true;
}
}
Loading

0 comments on commit 120c70d

Please sign in to comment.