Skip to content

Commit

Permalink
Add support for AWS rest-xml protocol
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
kstich committed Jan 14, 2020
1 parent c3d972d commit e184c6b
Show file tree
Hide file tree
Showing 10 changed files with 1,011 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ public class AddProtocols implements TypeScriptIntegration {

@Override
public List<ProtocolGenerator> getProtocolGenerators() {
return ListUtils.of(new AwsRestJson1_1(), new AwsJsonRpc1_0(), new AwsJsonRpc1_1());
return ListUtils.of(new AwsRestJson1_1(), new AwsJsonRpc1_0(), new AwsJsonRpc1_1(), new AwsRestXml());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

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

import static software.amazon.smithy.typescript.codegen.TypeScriptDependency.DEV_DEPENDENCY;
import static software.amazon.smithy.typescript.codegen.TypeScriptDependency.NORMAL_DEPENDENCY;

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

public final String packageName;
public final String version;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

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

import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import software.amazon.smithy.aws.traits.UnsignedPayloadTrait;
Expand All @@ -23,6 +24,7 @@
import software.amazon.smithy.model.shapes.OperationShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeVisitor;
import software.amazon.smithy.model.traits.XmlNamespaceTrait;
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext;

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

writer.write("");
}

/**
* Writes an attribute containing information about a Shape's optionally specified
* XML namespace configuration to an attribute of the passed node name.
*
* @param context The generation context.
* @param shape The shape to apply the namespace attribute to, if present on it.
* @param nodeName The node to apply the namespace attribute to.
*/
static void writeXmlNamespace(GenerationContext context, Shape shape, String nodeName) {
shape.getTrait(XmlNamespaceTrait.class).ifPresent(trait -> {
TypeScriptWriter writer = context.getWriter();
String xmlns = "xmlns";
Optional<String> prefix = trait.getPrefix();
if (prefix.isPresent()) {
xmlns += ":" + prefix.get();
}
writer.write("$L.addAttribute($S, $S);", nodeName, xmlns, trait.getUri());
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
/*
* 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.List;
import java.util.Set;
import software.amazon.smithy.aws.traits.ServiceTrait;
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.codegen.core.SymbolReference;
import software.amazon.smithy.model.knowledge.HttpBinding;
import software.amazon.smithy.model.knowledge.HttpBinding.Location;
import software.amazon.smithy.model.shapes.MemberShape;
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.shapes.UnionShape;
import software.amazon.smithy.model.traits.TimestampFormatTrait.Format;
import software.amazon.smithy.model.traits.XmlNamespaceTrait;
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
import software.amazon.smithy.typescript.codegen.integration.HttpBindingProtocolGenerator;

/**
* Handles generating the aws.rest-xml 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 HttpBindingProtocolGenerator} to handle
* components of binding to HTTP requests and responses.
*
* A service-specific customization exists for Amazon S3, which doesn't wrap the Error
* object in the response.
*
* TODO: Build an XmlIndex to handle pre-computing resolved values for names, namespaces, and more.
*
* @see XmlShapeSerVisitor
* @see XmlShapeDeserVisitor
* @see XmlMemberSerVisitor
* @see XmlMemberDeserVisitor
* @see AwsProtocolUtils
* @see <a href="https://awslabs.github.io/smithy/spec/http.html">Smithy HTTP protocol bindings.</a>
* @see <a href="https://awslabs.github.io/smithy/spec/xml.html">Smithy XML traits.</a>
*/
final class AwsRestXml extends HttpBindingProtocolGenerator {

AwsRestXml() {
super(true);
}

@Override
protected String getDocumentContentType() {
return "application/xml";
}

@Override
protected Format getDocumentTimestampFormat() {
return Format.DATE_TIME;
}

@Override
public String getName() {
return "aws.rest-xml";
}

@Override
protected void generateDocumentBodyShapeSerializers(GenerationContext context, Set<Shape> shapes) {
AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new XmlShapeSerVisitor(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);

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

// Include an XML body parser used to deserialize documents from HTTP responses.
writer.addImport("SerdeContext", "__SerdeContext", "@aws-sdk/types");
writer.addDependency(AwsDependency.XML_PARSER);
writer.addDependency(AwsDependency.XML_PARSER_TYPES);
writer.addImport("parse", "pixlParse", "pixl-xml");
writer.openBlock("const parseBody = (streamBody: any, context: __SerdeContext): any => {", "};", () -> {
writer.openBlock("return collectBodyString(streamBody, context).then(encoded => {", "});", () -> {
writer.openBlock("if (encoded.length) {", "}", () -> {
writer.write("return pixlParse(encoded);");
});
writer.write("return {};");
});
});

writer.write("");

// Generate a function that handles the complex rules around deserializing
// an error code from a rest-xml error.
SymbolReference responseType = getApplicationProtocol().getResponseType();
writer.openBlock("const loadRestXmlErrorCode = (\n"
+ " output: $T,\n"
+ " data: any\n"
+ "): string => {", "};", responseType, () -> {
// Start building the location that contains the error code.
StringBuilder locationBuilder = new StringBuilder("data.");
// Some services, S3 for example, don't wrap the Error object in the response.
if (usesWrappedErrorResponse(context)) {
locationBuilder.append("Error.");
}
locationBuilder.append("Code");

// Attempt to fetch the error code from the specific location.
String errorCodeLocation = locationBuilder.toString();
writer.openBlock("if ($L !== undefined) {", "}", errorCodeLocation, () -> {
writer.write("return $L;", errorCodeLocation);
});

// 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';");
});
writer.write("");
}

private boolean usesWrappedErrorResponse(GenerationContext context) {
return context.getService().getTrait(ServiceTrait.class)
.map(trait -> !trait.getSdkId().equals("S3"))
.orElse(true);
}

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

@Override
protected void serializeInputDocument(
GenerationContext context,
OperationShape operation,
List<HttpBinding> documentBindings
) {
SymbolProvider symbolProvider = context.getSymbolProvider();
TypeScriptWriter writer = context.getWriter();

// Start with the XML declaration.
writer.write("body = \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\";");

writer.addImport("XmlNode", "__XmlNode", "@aws-sdk/xml-builder");
writer.write("const bodyNode = new __XmlNode($S);", operation.getId().getName());

// Always add @xmlNamespace value of the service to the root node, since we're
// creating a wrapper node not based on a structure.
AwsProtocolUtils.writeXmlNamespace(context, context.getService(), "bodyNode");

XmlShapeSerVisitor shapeSerVisitor = new XmlShapeSerVisitor(context);

for (HttpBinding binding : documentBindings) {
MemberShape memberShape = binding.getMember();
// The name of the member to get from the input shape.
String memberName = symbolProvider.toMemberName(memberShape);

String inputLocation = "input." + memberName;
writer.openBlock("if ($L !== undefined) {", "}", inputLocation, () -> {
shapeSerVisitor.serializeNamedMember(context, memberName, memberShape, () -> inputLocation);
});
}

// Append the generated XML to the body.
writer.write("body += bodyNode.toString();");
}

@Override
protected void serializeInputPayload(
GenerationContext context,
OperationShape operation,
HttpBinding payloadBinding
) {
SymbolProvider symbolProvider = context.getSymbolProvider();
TypeScriptWriter writer = context.getWriter();

MemberShape member = payloadBinding.getMember();
String memberName = symbolProvider.toMemberName(member);

writer.write("let contents: any;");

// Generate an if statement to set the body node if the member is set.
writer.openBlock("if (input.$L !== undefined) {", "}", memberName, () -> {
Shape target = context.getModel().expectShape(member.getTarget());
writer.write("contents = $L;",
getInputValue(context, Location.PAYLOAD, "input." + memberName, member, target));

// Structure and Union payloads will serialize as XML documents via XmlNode.
if (target instanceof StructureShape || target instanceof UnionShape) {
// Start with the XML declaration.
writer.write("body = \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\";");

// Add @xmlNamespace value of the service to the root structure if one doesn't
// exist on the target we're serializing.
if (!target.hasTrait(XmlNamespaceTrait.class)) {
AwsProtocolUtils.writeXmlNamespace(context, context.getService(), "contents");
}

// Append the generated XML to the body.
writer.write("body += contents.toString();");
} else {
// Strings and blobs (streaming or not) will not need any modification.
writer.write("body = contents;");
}
});
}

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

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

@Override
protected void deserializeOutputDocument(
GenerationContext context,
Shape operationOrError,
List<HttpBinding> documentBindings
) {
SymbolProvider symbolProvider = context.getSymbolProvider();
XmlShapeDeserVisitor shapeDeserVisitor = new XmlShapeDeserVisitor(context);

for (HttpBinding binding : documentBindings) {
MemberShape memberShape = binding.getMember();
// The name of the member to get from the output shape.
String memberName = symbolProvider.toMemberName(memberShape);

shapeDeserVisitor.deserializeNamedStructureMember(context, memberName, memberShape, () -> "data");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
/**
* Overrides the default implementation of BigDecimal and BigInteger shape
* serialization to throw when encountered in AWS JSON based protocols.
*
* TODO: Work out support for BigDecimal and BigInteger, natively or through a library.
*/
final class JsonMemberSerVisitor extends DocumentMemberSerVisitor {

Expand Down
Loading

0 comments on commit e184c6b

Please sign in to comment.