Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for the aws.ec2 protocol #791

Merged
merged 2 commits into from
Jan 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
kstich marked this conversation as resolved.
Show resolved Hide resolved
.orElseGet(() -> StringUtils.capitalize(defaultValue));
}

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