From d87616471176a4a1ba588c14d2d57212c7574259 Mon Sep 17 00:00:00 2001 From: stickevi Date: Mon, 13 Jan 2020 13:25:58 -0800 Subject: [PATCH] Add support for AWS rest-xml protocol This commit adds support for the `aws.rest-xml` protocol, building on top of the `HttpBindingProtocolGenerator`. It includes an additional abstract class, `RestXmlResponseProtocolGenerator`, to aid in implementation of other protocols that use XML bindings for their HTTP responses. 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. --- .../aws/typescript/codegen/AddProtocols.java | 2 +- .../aws/typescript/codegen/AwsDependency.java | 6 +- .../typescript/codegen/AwsProtocolUtils.java | 22 ++ .../aws/typescript/codegen/AwsRestXml.java | 150 ++++++++++ .../RestXmlResponseProtocolGenerator.java | 137 +++++++++ .../codegen/XmlMemberDeserVisitor.java | 111 ++++++++ .../codegen/XmlMemberSerVisitor.java | 131 +++++++++ .../codegen/XmlShapeDeserVisitor.java | 207 ++++++++++++++ .../codegen/XmlShapeSerVisitor.java | 259 ++++++++++++++++++ packages/xml-builder/src/XmlNode.ts | 7 +- 10 files changed, 1029 insertions(+), 3 deletions(-) create mode 100644 codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsRestXml.java create mode 100644 codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/RestXmlResponseProtocolGenerator.java create mode 100644 codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlMemberDeserVisitor.java create mode 100644 codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlMemberSerVisitor.java create mode 100644 codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlShapeDeserVisitor.java create mode 100644 codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlShapeSerVisitor.java diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddProtocols.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddProtocols.java index 1bdf71fba0d2c..e8de20ba743b8 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddProtocols.java +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddProtocols.java @@ -27,6 +27,6 @@ public class AddProtocols implements TypeScriptIntegration { @Override public List 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()); } } diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsDependency.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsDependency.java index 24b6269e34960..bb009a55796e4 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsDependency.java +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsDependency.java @@ -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; @@ -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; diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsProtocolUtils.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsProtocolUtils.java index 3db4e51f42f71..da05f18e5a491 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsProtocolUtils.java +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsProtocolUtils.java @@ -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; @@ -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; @@ -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 prefix = trait.getPrefix(); + if (prefix.isPresent()) { + xmlns += ":" + prefix.get(); + } + writer.write("$L.addAttribute($S, $S);", nodeName, xmlns, trait.getUri()); + }); + } } diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsRestXml.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsRestXml.java new file mode 100644 index 0000000000000..6c7fdbaa6f9db --- /dev/null +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AwsRestXml.java @@ -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.List; +import java.util.Set; +import software.amazon.smithy.codegen.core.SymbolProvider; +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.typescript.codegen.TypeScriptWriter; + +/** + * Handles generating the aws.rest-xml protocol for services. + * + * This builds on the foundations of the {@link RestXmlResponseProtocolGenerator} to handle + * components of binding to HTTP responses and of the {@link HttpBindingProtocolGenerator} + * to handle components of binding to HTTP requests. + * + * @see XmlShapeSerVisitor + * @see XmlMemberSerVisitor + * @see AwsProtocolUtils + * @see Smithy HTTP protocol bindings. + * @see Smithy XML traits. + */ +final class AwsRestXml extends RestXmlResponseProtocolGenerator { + + @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 shapes) { + AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new XmlShapeSerVisitor(context)); + } + + @Override + public void generateSharedComponents(GenerationContext context) { + super.generateSharedComponents(context); + + TypeScriptWriter writer = context.getWriter(); + writer.addDependency(AwsDependency.XML_BUILDER); + } + + @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 documentBindings + ) { + SymbolProvider symbolProvider = context.getSymbolProvider(); + TypeScriptWriter writer = context.getWriter(); + + // Start with the XML declaration. + writer.write("body = \"\";"); + + writer.addImport("XmlNode", "__XmlNode", "@aws-sdk/xml-builder"); + writer.write("const bodyNode = new __XmlNode($S);", operation.getId().getName()); + + // Add @xmlNamespace value of the service to the root node. + 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 = \"\";"); + // Add @xmlNamespace value of the service to the root structure. + 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;"); + } + }); + } +} diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/RestXmlResponseProtocolGenerator.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/RestXmlResponseProtocolGenerator.java new file mode 100644 index 0000000000000..998cb269df02d --- /dev/null +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/RestXmlResponseProtocolGenerator.java @@ -0,0 +1,137 @@ +/* + * 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.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.typescript.codegen.TypeScriptWriter; +import software.amazon.smithy.typescript.codegen.integration.HttpBindingProtocolGenerator; + +/** + * Handles general components across the XML-document response based AWS protocols + * that have HTTP bindings. It handles reading from document bodies, including + * generating any functions needed for performing deserialization. + * + * This builds on the foundations of the {@link HttpBindingProtocolGenerator} to + * handle components of binding to HTTP responses. + * + * @see XmlShapeDeserVisitor + * @see XmlMemberDeserVisitor + * @see AwsProtocolUtils + * @see Smithy HTTP protocol bindings. + * @see Smithy XML traits. + */ +abstract class RestXmlResponseProtocolGenerator extends HttpBindingProtocolGenerator { + + RestXmlResponseProtocolGenerator() { + super(true); + } + + @Override + protected void generateDocumentBodyShapeDeserializers(GenerationContext context, Set shapes) { + AwsProtocolUtils.generateDocumentBodyShapeSerde(context, shapes, new XmlShapeDeserVisitor(context)); + } + + @Override + public void generateSharedComponents(GenerationContext context) { + super.generateSharedComponents(context); + + TypeScriptWriter writer = context.getWriter(); + + // 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 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 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"); + } + } +} diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlMemberDeserVisitor.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlMemberDeserVisitor.java new file mode 100644 index 0000000000000..8c6c92cee1753 --- /dev/null +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlMemberDeserVisitor.java @@ -0,0 +1,111 @@ +/* + * 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 software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.model.shapes.BigDecimalShape; +import software.amazon.smithy.model.shapes.BigIntegerShape; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.ByteShape; +import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.LongShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShortShape; +import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; +import software.amazon.smithy.typescript.codegen.integration.DocumentMemberDeserVisitor; +import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; + +/** + * Overrides several of the default implementations to handle XML document + * contents deserializing to strings instead of typed components: + * + *
    + *
  • Uses {@code parseFloat} on Float and Double shapes.
  • + *
  • Fails on BigDecimal and BigInteger shapes.
  • + *
  • Uses {@code parseInt} on other number shapes.
  • + *
  • Compares boolean shapes to the string {@code "true"} to generate a boolean.
  • + *
+ * + * @see Smithy XML traits. + */ +final class XmlMemberDeserVisitor extends DocumentMemberDeserVisitor { + + XmlMemberDeserVisitor(GenerationContext context, String dataSource, Format defaultTimestampFormat) { + super(context, dataSource, defaultTimestampFormat); + } + + @Override + public String booleanShape(BooleanShape shape) { + return getDataSource() + " == 'true'"; + } + + @Override + public String byteShape(ByteShape shape) { + return deserializeInt(); + } + + @Override + public String shortShape(ShortShape shape) { + return deserializeInt(); + } + + @Override + public String integerShape(IntegerShape shape) { + return deserializeInt(); + } + + @Override + public String longShape(LongShape shape) { + return deserializeInt(); + } + + private String deserializeInt() { + return "parseInt(" + getDataSource() + ")"; + } + + @Override + public String floatShape(FloatShape shape) { + return deserializeFloat(); + } + + @Override + public String doubleShape(DoubleShape shape) { + return deserializeFloat(); + } + + private String deserializeFloat() { + return "parseFloat(" + getDataSource() + ")"; + } + + @Override + public String bigDecimalShape(BigDecimalShape shape) { + // Fail instead of losing precision through Number. + return unsupportedShape(shape); + } + + @Override + public String bigIntegerShape(BigIntegerShape shape) { + // Fail instead of losing precision through Number. + return unsupportedShape(shape); + } + + private String unsupportedShape(Shape shape) { + throw new CodegenException(String.format("Cannot deserialize shape type %s on protocol, shape: %s.", + shape.getType(), shape.getId())); + } +} diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlMemberSerVisitor.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlMemberSerVisitor.java new file mode 100644 index 0000000000000..3e94493a0dee8 --- /dev/null +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlMemberSerVisitor.java @@ -0,0 +1,131 @@ +/* + * 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 software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.model.shapes.BigDecimalShape; +import software.amazon.smithy.model.shapes.BigIntegerShape; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.ByteShape; +import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.LongShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShortShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.TimestampShape; +import software.amazon.smithy.model.traits.TimestampFormatTrait.Format; +import software.amazon.smithy.model.traits.XmlNameTrait; +import software.amazon.smithy.typescript.codegen.TypeScriptWriter; +import software.amazon.smithy.typescript.codegen.integration.DocumentMemberSerVisitor; +import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; + +/** + * Overrides several of the default implementations to handle XML document + * contents serializing as XmlText entities instead of typed components: + * + *
    + *
  • All scalar types are set as contents of an XmlText inside an XmlNode.
  • + *
  • Fails on BigDecimal and BigInteger shapes.
  • + *
+ * + * @see Smithy XML traits. + */ +final class XmlMemberSerVisitor extends DocumentMemberSerVisitor { + + XmlMemberSerVisitor(GenerationContext context, String dataSource, Format defaultTimestampFormat) { + super(context, dataSource, defaultTimestampFormat); + } + + @Override + public String stringShape(StringShape shape) { + return getAsXmlText(shape, super.stringShape(shape)); + } + + @Override + public String timestampShape(TimestampShape shape) { + return getAsXmlText(shape, super.timestampShape(shape)); + } + + @Override + public String booleanShape(BooleanShape shape) { + return serializeSimpleScalar(shape); + } + + @Override + public String byteShape(ByteShape shape) { + return serializeSimpleScalar(shape); + } + + @Override + public String shortShape(ShortShape shape) { + return serializeSimpleScalar(shape); + } + + @Override + public String integerShape(IntegerShape shape) { + return serializeSimpleScalar(shape); + } + + @Override + public String longShape(LongShape shape) { + return serializeSimpleScalar(shape); + } + + @Override + public String floatShape(FloatShape shape) { + return serializeSimpleScalar(shape); + } + + @Override + public String doubleShape(DoubleShape shape) { + return serializeSimpleScalar(shape); + } + + private String serializeSimpleScalar(Shape shape) { + return getAsXmlText(shape, "String(" + getDataSource() + ")"); + } + + private String getAsXmlText(Shape shape, String dataSource) { + // Handle the @xmlName trait for the shape itself. + String nodeName = shape.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse(shape.getId().getName()); + + TypeScriptWriter writer = getContext().getWriter(); + writer.addImport("XmlNode", "__XmlNode", "@aws-sdk/xml-builder"); + writer.addImport("XmlText", "__XmlText", "@aws-sdk/xml-builder"); + return "new __XmlNode(\"" + nodeName + "\").addChildNode(new __XmlText(" + dataSource + "))"; + } + + @Override + public String bigDecimalShape(BigDecimalShape shape) { + // Fail instead of losing precision through Number. + return unsupportedShape(shape); + } + + @Override + public String bigIntegerShape(BigIntegerShape shape) { + // Fail instead of losing precision through Number. + return unsupportedShape(shape); + } + + private String unsupportedShape(Shape shape) { + throw new CodegenException(String.format("Cannot serialize shape type %s on protocol, shape: %s.", + shape.getType(), shape.getId())); + } +} diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlShapeDeserVisitor.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlShapeDeserVisitor.java new file mode 100644 index 0000000000000..f3f709a4cff0b --- /dev/null +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlShapeDeserVisitor.java @@ -0,0 +1,207 @@ +/* + * 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.Map; +import java.util.TreeMap; +import java.util.function.Supplier; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.CollectionShape; +import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +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.XmlFlattenedTrait; +import software.amazon.smithy.model.traits.XmlNameTrait; +import software.amazon.smithy.typescript.codegen.TypeScriptWriter; +import software.amazon.smithy.typescript.codegen.integration.DocumentMemberDeserVisitor; +import software.amazon.smithy.typescript.codegen.integration.DocumentShapeDeserVisitor; +import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; + +/** + * Visitor to generate deserialization functions for shapes in XML-document + * based document bodies. + * + * No standard visitation methods are overridden; function body generation for all + * expected deserializers is handled by this class. + * + * Timestamps are deserialized from {@link Format}.DATE_TIME by default. + * + * @see Smithy XML traits. + */ +final class XmlShapeDeserVisitor extends DocumentShapeDeserVisitor { + + XmlShapeDeserVisitor(GenerationContext context) { + super(context); + } + + private DocumentMemberDeserVisitor getMemberVisitor(String dataSource) { + return new XmlMemberDeserVisitor(getContext(), dataSource, Format.DATE_TIME); + } + + @Override + protected void deserializeCollection(GenerationContext context, CollectionShape shape) { + TypeScriptWriter writer = context.getWriter(); + Shape target = context.getModel().expectShape(shape.getMember().getTarget()); + + // Dispatch to the output value provider for any additional handling. + writer.openBlock("return (output || []).map((entry: any) =>", ");", () -> { + writer.write(target.accept(getMemberVisitor("entry"))); + }); + } + + @Override + protected void deserializeDocument(GenerationContext context, DocumentShape shape) { + throw new CodegenException(String.format("Cannot deserialize Document types on AWS XML protocols, shape: %s.", + shape.getId())); + } + + @Override + protected void deserializeMap(GenerationContext context, MapShape shape) { + TypeScriptWriter writer = context.getWriter(); + Shape target = context.getModel().expectShape(shape.getValue().getTarget()); + + // Get the right serialization for each entry in the map. Undefined + // outputs won't have this deserializer invoked. + writer.write("let mapParams: any = {};"); + writer.openBlock("Object.keys(output).forEach(key => {", "});", () -> { + // Dispatch to the output value provider for any additional handling. + writer.write("mapParams[key] = $L;", target.accept(getMemberVisitor("output[key]"))); + }); + writer.write("return mapParams;"); + } + + @Override + protected void deserializeStructure(GenerationContext context, StructureShape shape) { + TypeScriptWriter writer = context.getWriter(); + + // Prepare the document contents structure. + // Use a TreeMap to sort the members. + Map members = new TreeMap<>(shape.getAllMembers()); + writer.openBlock("let contents: any = {", "};", () -> { + writer.write("__type: $S,", shape.getId().getName()); + // Set all the members to undefined to meet type constraints. + members.forEach((memberName, memberShape) -> writer.write("$L: undefined,", memberName)); + }); + + members.forEach((memberName, memberShape) -> + deserializeNamedStructureMember(context, memberName, memberShape, () -> "output")); + + writer.write("return contents;"); + } + + void deserializeNamedStructureMember( + GenerationContext context, + String memberName, + MemberShape memberShape, + Supplier inputLocation + ) { + TypeScriptWriter writer = context.getWriter(); + Model model = context.getModel(); + + // Use the @xmlName trait if present on the member, otherwise use the member name. + String locationName = memberShape.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse(memberName); + // Grab the target shape so we can use a member deserializer on it. + Shape target = context.getModel().expectShape(memberShape.getTarget()); + // Handle @xmlFlattened for collections and maps. + boolean isFlattened = memberShape.hasTrait(XmlFlattenedTrait.class); + + // Build a string based on the traits that represents the location. + // Note we don't need to handle @xmlAttribute here because the parser flattens + // attributes in to the main structure. + StringBuilder sourceBuilder = new StringBuilder(inputLocation.get()) + .append("['").append(locationName).append("']"); + + // Go in to a specialized element for unflattened aggregates + if (deserializationReturnsArray(target) && !isFlattened) { + String targetLocation = getUnnamedAggregateTargetLocation(model, target); + sourceBuilder.append("['").append(targetLocation).append("']"); + } + + // Handle the response property. + String source = sourceBuilder.toString(); + writer.openBlock("if ($L !== undefined) {", "}", source, () -> { + if (isFlattened) { + writer.write("const wrappedItem = ($1L instanceof Array) ? $1L : [$1L];", source); + } + writer.write("contents.$L = $L;", memberName, + // Dispatch to the output value provider for any additional handling. + target.accept(getMemberVisitor(isFlattened ? "wrappedItem" : source))); + }); + } + + private boolean deserializationReturnsArray(Shape shape) { + return (shape instanceof CollectionShape) || (shape instanceof MapShape); + } + + private String getUnnamedAggregateTargetLocation(Model model, Shape shape) { + return shape.isMapShape() + ? "entry" + : ((CollectionShape) shape).getMember().getMemberTrait(model, XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse("member"); + } + + @Override + protected void deserializeUnion(GenerationContext context, UnionShape shape) { + TypeScriptWriter writer = context.getWriter(); + Model model = context.getModel(); + + // Check for any known union members and return when we find one. + Map members = new TreeMap<>(shape.getAllMembers()); + members.forEach((memberName, memberShape) -> { + // Use the @xmlName trait if present on the member, otherwise use the member name. + String locationName = memberShape.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse(memberName); + // Grab the target shape so we can use a member deserializer on it. + Shape target = context.getModel() + .expectShape(memberShape.getTarget()); + // Handle @xmlFlattened for collections and maps. + boolean isFlattened = memberShape.hasTrait(XmlFlattenedTrait.class); + + // Build a string based on the traits that represents the location. + // Note we don't need to handle @xmlAttribute here because the parser flattens + // attributes in to the main structure. + StringBuilder sourceBuilder = new StringBuilder("output['").append(locationName).append("']"); + + // Go in to a specialized element for unflattened aggregates + if (deserializationReturnsArray(target) && !isFlattened) { + String targetLocation = getUnnamedAggregateTargetLocation(model, target); + sourceBuilder.append("['").append(targetLocation).append("']"); + } + + // Handle the response property. + String source = sourceBuilder.toString(); + writer.openBlock("if ($L !== undefined) {", "}", source, () -> { + writer.openBlock("return {", "};", () -> { + // Dispatch to the output value provider for any additional handling. + writer.write("$L: $L", memberName, target.accept(getMemberVisitor(source))); + }); + }); + }); + + // Or write output element to the unknown member. + writer.write("const key = Object.keys(output)[0];"); + writer.write("return { $$unknown: [key, output[key]] };"); + } +} diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlShapeSerVisitor.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlShapeSerVisitor.java new file mode 100644 index 0000000000000..b0d9f20cb5f22 --- /dev/null +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/XmlShapeSerVisitor.java @@ -0,0 +1,259 @@ +/* + * 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.Map; +import java.util.TreeMap; +import java.util.function.Supplier; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.CollectionShape; +import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +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.XmlAttributeTrait; +import software.amazon.smithy.model.traits.XmlFlattenedTrait; +import software.amazon.smithy.model.traits.XmlNameTrait; +import software.amazon.smithy.typescript.codegen.TypeScriptWriter; +import software.amazon.smithy.typescript.codegen.integration.DocumentMemberSerVisitor; +import software.amazon.smithy.typescript.codegen.integration.DocumentShapeSerVisitor; +import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator.GenerationContext; + +/** + * Visitor to generate deserialization functions for shapes in XML-document + * based document bodies. + * + * No standard visitation methods are overridden; function body generation for all + * expected serializers is handled by this class. + * + * Timestamps are serialized to {@link Format}.DATE_TIME by default. + * + * @see Smithy XML traits. + */ +final class XmlShapeSerVisitor extends DocumentShapeSerVisitor { + + XmlShapeSerVisitor(GenerationContext context) { + super(context); + } + + private DocumentMemberSerVisitor getMemberVisitor(String dataSource) { + return new XmlMemberSerVisitor(getContext(), dataSource, Format.DATE_TIME); + } + + @Override + protected void serializeCollection(GenerationContext context, CollectionShape shape) { + TypeScriptWriter writer = context.getWriter(); + MemberShape memberShape = shape.getMember(); + Shape target = context.getModel().expectShape(memberShape.getTarget()); + writer.addImport("XmlNode", "__XmlNode", "@aws-sdk/xml-builder"); + + // Use the @xmlName trait if present on the member, otherwise use "member". + String locationName = memberShape.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse("member"); + + // Set up a location to store all of the child node(s). + writer.write("const collectedNodes: any = [];"); + // Dispatch to the input value provider for any additional handling. + writer.openBlock("(input || []).map(entry => {", "});", () -> { + writer.write("const node = $L;", target.accept(getMemberVisitor("entry"))); + writer.write("collectedNodes.push(node.withName($S));", locationName); + }); + + writer.write("return collectedNodes;"); + } + + @Override + protected void serializeDocument(GenerationContext context, DocumentShape shape) { + throw new CodegenException(String.format( + "Cannot serialize Document types on AWS XML protocols, shape: %s.", shape.getId())); + } + + @Override + protected void serializeMap(GenerationContext context, MapShape shape) { + TypeScriptWriter writer = context.getWriter(); + Model model = context.getModel(); + writer.addImport("XmlNode", "__XmlNode", "@aws-sdk/xml-builder"); + + // Set up a location to store all of the child node(s). + writer.write("const collectedNodes: any = [];"); + // Use the keys as an iteration point to dispatch to the input value providers. + writer.openBlock("Object.keys(input).forEach(key => {", "});", () -> { + // Prepare a containing node for each entry's k/v pair. + writer.write("const entryNode = new __XmlNode(\"entry\");"); + + // Prepare the key's node. + // Use the @xmlName trait if present on the member, otherwise use "key". + MemberShape keyMember = shape.getKey(); + Shape keyTarget = model.expectShape(keyMember.getTarget()); + String keyName = keyMember.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse("key"); + writer.write("const keyNode = new __XmlNode($S');", keyName); + writer.write("keyNode.addChildNode($L)", keyTarget.accept(getMemberVisitor("key"))); + writer.write("entryNode.addChildNode(keyNode);"); + + // Prepare the value's node. + // Use the @xmlName trait if present on the member, otherwise use "value". + MemberShape valueMember = shape.getValue(); + Shape valueTarget = model.expectShape(valueMember.getTarget()); + String valueName = valueMember.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse("key"); + writer.write("const keyNode = new __XmlNode($S');", valueName); + writer.write("keyNode.addChildNode($L)", valueTarget.accept(getMemberVisitor("input[key]"))); + writer.write("entryNode.addChildNode(keyNode);"); + + // Add the entry to the collection. + writer.write("collectedNodes.push(entryNode);"); + }); + + writer.write("return collectedNodes;"); + } + + @Override + protected void serializeStructure(GenerationContext context, StructureShape shape) { + TypeScriptWriter writer = context.getWriter(); + writer.addImport("XmlNode", "__XmlNode", "@aws-sdk/xml-builder"); + + // Handle the @xmlName trait for the structure itself. + String nodeName = shape.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse(shape.getId().getName()); + + // Create the structure's node. + writer.write("const bodyNode = new __XmlNode($S);", nodeName); + + // Add @xmlNamespace value of the structure to the node. + AwsProtocolUtils.writeXmlNamespace(context, shape, "bodyNode"); + + // Serialize every member of the structure if present. + Map members = new TreeMap<>(shape.getAllMembers()); + members.forEach((memberName, memberShape) -> { + String inputLocation = "input." + memberName; + writer.openBlock("if ($L !== undefined) {", "}", inputLocation, () -> { + serializeNamedMember(context, memberName, memberShape, () -> inputLocation); + }); + }); + + writer.write("return bodyNode;"); + } + + void serializeNamedMember( + GenerationContext context, + String memberName, + MemberShape memberShape, + Supplier inputLocation + ) { + TypeScriptWriter writer = context.getWriter(); + DocumentMemberSerVisitor inputVisitor = getMemberVisitor(inputLocation.get()); + + // Use the @xmlName trait if present on the member, otherwise use the member name. + String locationName = memberShape.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse(memberName); + // Grab the target shape so we can use a member serializer on it. + Shape target = context.getModel().expectShape(memberShape.getTarget()); + + // Handle @xmlAttribute simple members. + if (memberShape.hasTrait(XmlAttributeTrait.class)) { + writer.write("bodyNode.addAttribute($S, $L);", locationName, inputLocation.get()); + } else { + // Collected members must be handled with flattening and renaming. + if (serializationReturnsArray(target)) { + // Handle @xmlFlattened for collections and maps. + boolean isFlattened = memberShape.hasTrait(XmlFlattenedTrait.class); + + // Get all the nodes that are going to be serialized. + writer.write("const nodes = $L;", target.accept(inputVisitor)); + + // Prepare a containing node to hold the nodes if not flattened. + if (!isFlattened) { + writer.write("const containerNode = new __XmlNode($S);", locationName); + } + + // Add every node to the target node. + writer.openBlock("nodes.map((node: any) => {", "});", () -> { + // Adjust to add sub nodes to the right target based on flattening. + String targetNode = isFlattened ? "bodyNode" : "containerNode"; + if (isFlattened) { + writer.write("node = node.withName($S);", locationName); + } + writer.write("$L.addChildNode(node);", targetNode); + }); + + // For non-flattened collected nodes, we have to add the containing node. + if (!isFlattened) { + writer.write("bodyNode.addChildNode(containerNode);"); + } + } else { + // Standard members are added as children after updating their names. + writer.write("const memberNode = $L;", target.accept(inputVisitor)); + writer.write("bodyNode.addChildNode(memberNode.withName($S));", locationName); + } + } + } + + private boolean serializationReturnsArray(Shape shape) { + return (shape instanceof CollectionShape) || (shape instanceof MapShape); + } + + @Override + protected void serializeUnion(GenerationContext context, UnionShape shape) { + TypeScriptWriter writer = context.getWriter(); + writer.addImport("XmlNode", "__XmlNode", "@aws-sdk/xml-builder"); + writer.addImport("XmlText", "__XmlText", "@aws-sdk/xml-builder"); + + // Handle the @xmlName trait for the union itself. + String nodeName = shape.getTrait(XmlNameTrait.class) + .map(XmlNameTrait::getValue) + .orElse(shape.getId().getName()); + + // Create the union's node. + writer.write("const bodyNode = new __XmlNode($S);", nodeName); + + // Add @xmlNamespace value of the structure to the node. + AwsProtocolUtils.writeXmlNamespace(context, shape, "bodyNode"); + + // Visit over the union type, then get the right serialization for the member. + writer.openBlock("$L.visit(input, {", "});", shape.getId().getName(), () -> { + // Use a TreeMap to sort the members. + Map members = new TreeMap<>(shape.getAllMembers()); + members.forEach((memberName, memberShape) -> { + writer.openBlock("$L: value => {", "},", memberName, () -> { + serializeNamedMember(context, memberName, memberShape, () -> "value"); + }); + }); + + // Handle the unknown property. + writer.openBlock("_: (name: string, value: any) => {", "}", () -> { + // Throw an exception if we're trying to serialize something that we wouldn't know how to. + writer.openBlock("if (!(value instanceof __XmlNode || value instanceof __XmlText)) {", "}", () -> { + writer.write("throw new Error(\"Unable to serialize unknown union members in XML.\");"); + }); + + // Set the node explicitly for potentially correct cases. + writer.write("bodyNode.addChildNode(new __XmlNode(value).addChildNode(value));"); + }); + }); + + writer.write("return bodyNode;"); + } +} diff --git a/packages/xml-builder/src/XmlNode.ts b/packages/xml-builder/src/XmlNode.ts index cc02fc82704a2..a19cb48139119 100644 --- a/packages/xml-builder/src/XmlNode.ts +++ b/packages/xml-builder/src/XmlNode.ts @@ -10,7 +10,12 @@ export class XmlNode { constructor( private name: string, public readonly children: Stringable[] = [] - ) {} + ) { } + + withName(name: string): XmlNode { + this.name = name; + return this; + } addAttribute(name: string, value: any): XmlNode { this.attributes[name] = value;