From a87ef15659d457c607421ee90635d35b2e6def4c Mon Sep 17 00:00:00 2001 From: Sean McGrail Date: Thu, 8 Jul 2021 16:53:53 -0700 Subject: [PATCH] Implement Feedback --- .../smithy/go/codegen/CodegenVisitor.java | 11 +- .../amazon/smithy/go/codegen/GoWriter.java | 14 + .../go/codegen/ProtocolDocumentGenerator.java | 239 ++++++-- .../go/codegen/ShapeValueGenerator.java | 47 +- .../smithy/go/codegen/StructureGenerator.java | 5 + .../integration/ProtocolGenerator.java | 122 +++- .../go/codegen/integration/ProtocolUtils.java | 4 - .../go/codegen/document_doc.go.template | 63 ++ document/doc.go | 12 + document/document.go | 88 ++- document/errors.go | 7 +- document/internal/serde/field.go | 1 + document/internal/serde/field_cache.go | 3 + document/internal/serde/field_test.go | 2 +- document/internal/serde/reflect.go | 2 + document/internal/serde/serde.go | 3 + document/internal/serde/tags.go | 1 + document/json/decoder.go | 34 +- document/json/decoder_test.go | 539 ++---------------- document/json/doc.go | 8 + document/json/encoder.go | 24 +- document/json/json.go | 2 + document/json/shared_test.go | 481 ++++++++++++++++ testing/struct.go | 4 +- 24 files changed, 1100 insertions(+), 616 deletions(-) create mode 100644 codegen/smithy-go-codegen/src/main/resources/software/amazon/smithy/go/codegen/document_doc.go.template create mode 100644 document/doc.go create mode 100644 document/json/doc.go create mode 100644 document/json/shared_test.go diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java index 2365d5ca8..a83f51ee9 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java @@ -39,7 +39,6 @@ import software.amazon.smithy.model.knowledge.ServiceIndex; import software.amazon.smithy.model.knowledge.TopDownIndex; import software.amazon.smithy.model.neighbor.Walker; -import software.amazon.smithy.model.shapes.DocumentShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; @@ -168,8 +167,8 @@ void execute() { shape.accept(this); } - // Generate standard DocumentShape interface - protocolDocumentGenerator.generateStandardTypes(); + // Generate any required types and functions need to support protocol documents. + protocolDocumentGenerator.generateDocumentSupport(); // Generate a struct to handle unknown tags in unions List unions = serviceShapes.stream() @@ -235,12 +234,6 @@ protected Void getDefault(Shape shape) { return null; } - @Override - public Void documentShape(DocumentShape shape) { - protocolDocumentGenerator.addDocumentShape(shape); - return null; - } - @Override public Void structureShape(StructureShape shape) { if (shape.getId().getNamespace().equals(CodegenUtils.getSyntheticTypeNamespace())) { diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java index 2e72df371..65f784d5b 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/GoWriter.java @@ -256,6 +256,20 @@ public GoWriter writePackageDocs(String docs) { return this; } + /** + * Writes the doc to the Go package docs that are written prior to the go package statement. This does not perform + * line wrapping and the provided formatting must be valid Go doc. + * + * @param docs documentation to write to package doc. + * @return writer + */ + public GoWriter writeRawPackageDocs(String docs) { + writeDocs(packageDocs, () -> { + packageDocs.write(docs); + }); + return this; + } + /** * Writes shape documentation comments if docs are present. * diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ProtocolDocumentGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ProtocolDocumentGenerator.java index ccc2ef211..9e8d97298 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ProtocolDocumentGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ProtocolDocumentGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * Copyright 2021 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. @@ -15,18 +15,21 @@ package software.amazon.smithy.go.codegen; -import java.util.Set; -import java.util.TreeSet; import java.util.function.Consumer; import software.amazon.smithy.codegen.core.Symbol; import software.amazon.smithy.go.codegen.integration.ProtocolGenerator; import software.amazon.smithy.go.codegen.integration.ProtocolGenerator.GenerationContext; import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.knowledge.NeighborProviderIndex; +import software.amazon.smithy.model.neighbor.Walker; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.IoUtils; /** - * Generates the Smithy document type for the service client. + * Generates the service's internal and external document Go packages. The document packages contain the service + * specific document Interface definition, protocol specific document marshaler and unmarshaller implementations for + * that interface, and constructors for creating service document types. */ public final class ProtocolDocumentGenerator { public static final String DOCUMENT_INTERFACE_NAME = "Interface"; @@ -41,11 +44,10 @@ public final class ProtocolDocumentGenerator { private static final String SERVICE_SMITHY_DOCUMENT_INTERFACE = "smithyDocument"; private static final String IS_SMITHY_DOCUMENT_METHOD = "isSmithyDocument"; - private final GoSettings settings; private final GoDelegator delegator; - private final Set documentShapes = new TreeSet<>(); private final Model model; + private final boolean hasDocumentShapes; public ProtocolDocumentGenerator( GoSettings settings, @@ -55,54 +57,100 @@ public ProtocolDocumentGenerator( this.settings = settings; this.model = model; this.delegator = delegator; - } - /** - * Get the set of document shapes for the service. - * - * @return the service's document shapes. - */ - public Set getDocumentShapes() { - return documentShapes; + NeighborProviderIndex index = NeighborProviderIndex.of(this.model); + Walker walker = new Walker(index.getProvider()); + boolean documentShapesPresent = false; + for (Shape shape : walker.walkShapes(settings.getService(model), relationship -> { + switch (relationship.getRelationshipType()) { + case OPERATION: + case STRUCTURE_MEMBER: + case MAP_VALUE: + case SET_MEMBER: + case UNION_MEMBER: + case LIST_MEMBER: + case ERROR: + case OUTPUT: + case INPUT: + case MEMBER_TARGET: + case MEMBER_CONTAINER: + return true; + default: + return false; + } + })) { + if (shape.asDocumentShape().isPresent()) { + documentShapesPresent = true; + break; + } + } + this.hasDocumentShapes = documentShapesPresent; } /** - * Returns whether the service has any document shapes. - * - * @return whether the service has one or more document types. + * Generates any required client types or functions to support protocol document types. */ - public boolean hasDocumentShapes() { - return getDocumentShapes().size() > 0; + public void generateDocumentSupport() { + generateNoSerdeType(); + generateInternalDocumentInterface(); + generateDocumentPackage(); } /** - * Add a document shape to the generator. + * Generates the publicly accessible service document package. This package contains a type alias definition + * for document interface, as well as a constructor function for creating a document marshaller. + *

+ * This package is not generated if the service does not have any document shapes in the model. * - * @param shape the document shape to add. - */ - public void addDocumentShape(DocumentShape shape) { - documentShapes.add(shape); - } - - /** + *

{@code
+     * // /document
+     * package document
+     *
+     * import (
+     *      internaldocument "/internal/document"
+     * )
+     *
+     * type Interface = internaldocument.Interface
      *
+     * func NewLazyDocument(v interface{}) Interface {
+     *      return internaldocument.NewDocumentMarshaler(v)
+     * }
+     * }
*/ - public void generateStandardTypes() { - generateNoSerdeType(); - generateInternalDocumentInterface(); - generateDocumentPackage(); - } - private void generateDocumentPackage() { - if (!hasDocumentShapes()) { + if (!this.hasDocumentShapes) { return; } + writeDocumentPackage("doc.go", writer -> { + String documentTemplate = IoUtils.readUtf8Resource(getClass(), "document_doc.go.template"); + writer.writeRawPackageDocs(documentTemplate); + }); + writeDocumentPackage("document.go", writer -> { + writer.writeDocs(String.format("%s defines a document which is a protocol-agnostic type which supports a " + + "JSON-like data-model. You can use this type to send UTF-8 strings, arbitrary precision " + + "numbers, booleans, nulls, a list of these values, and a map of UTF-8 strings to these " + + "values.", DOCUMENT_INTERFACE_NAME)); + writer.writeDocs(""); + writer.writeDocs(String.format("You create a document type using the %s function and passing it the Go " + + "type to marshal. When receiving a document in an API response, you use the " + + "document's UnmarshalSmithyDocument function to decode the response to your desired Go " + + "type. Unless documented specifically generated structure types in client packages or " + + "client types packages are not supported at this time. Such types embed a " + + "noSmithyDocumentSerde and will cause an error to be returned when attempting to send an " + + "API request.", NEW_LAZY_DOCUMENT)); + writer.writeDocs(""); + writer.writeDocs("For more information see the accompanying package documentation and linked references."); writer.write("type $L = $T", DOCUMENT_INTERFACE_NAME, getInternalDocumentSymbol(DOCUMENT_INTERFACE_NAME)) .write(""); + writer.writeDocs(String.format("You create document type using the %s function and passing it the Go " + + "type to be marshaled and sent to the service. The document marshaler supports semantics similar " + + "to the encoding/json Go standard library.", NEW_LAZY_DOCUMENT)); + writer.writeDocs(""); + writer.writeDocs("For more information see the accompanying package documentation and linked references."); writer.openBlock("func $L(v interface{}) $T {", "}", NEW_LAZY_DOCUMENT, getDocumentSymbol(DOCUMENT_INTERFACE_NAME), () -> { writer.write("return $T(v)", @@ -112,6 +160,28 @@ private void generateDocumentPackage() { }); } + /** + * Generates an unexported type alias for the {@code github.com/aws/smithy-go/document#NoSerde} type in both the + * service and types package. This allows for this type to be used as an embedded member in structures to + * prevent usage of generated Smithy structure shapes as document types. Additionally, since the member is + * unexported this prevents the need de-conflict naming collisions. + *

+ * These type aliases are always generated regardless of whether there are document shapes present in the model + * or not. + * + *

{@code
+     * package types
+     *
+     * type noSmithyDocumentSerde = smithydocument.NoSerde
+     *
+     * type ExampleStructureShape struct {
+     *      FieldOne *string
+     *
+     *      noSmithyDocumentSerde
+     * }
+     *
+     * }
+ */ private void generateNoSerdeType() { Symbol noSerde = SymbolUtils.createValueSymbolBuilder("NoSerde", SmithyGoDependency.SMITHY_DOCUMENT).build(); @@ -125,30 +195,52 @@ private void generateNoSerdeType() { }); } + /** + * Generates the document interface definition in the internal document package. + * + *
{@code
+     * import smithydocument "github.com/aws/smithy-go/document"
+     *
+     * type smithyDocument interface {
+     *      isSmithyDocument()
+     * }
+     *
+     * type Interface interface {
+     *      smithydocument.Marshaler
+     *      smithydocument.Unmarshaler
+     *      smithyDocument
+     * }
+     * }
+ */ private void generateInternalDocumentInterface() { - if (!hasDocumentShapes()) { + if (!this.hasDocumentShapes) { return; } - writeInternalDocumentPackage("document.go", writer -> { - Symbol serviceSmithyDocumentInterface = getInternalDocumentSymbol(SERVICE_SMITHY_DOCUMENT_INTERFACE); + Symbol serviceSmithyDocumentInterface = getInternalDocumentSymbol(SERVICE_SMITHY_DOCUMENT_INTERFACE); + Symbol internalDocumentInterface = getInternalDocumentSymbol(DOCUMENT_INTERFACE_NAME); + Symbol smithyDocumentMarshaler = SymbolUtils.createValueSymbolBuilder("Marshaler", + SmithyGoDependency.SMITHY_DOCUMENT).build(); + Symbol smithyDocumentUnmarshaler = SymbolUtils.createValueSymbolBuilder("Unmarshaler", + SmithyGoDependency.SMITHY_DOCUMENT).build(); + writeInternalDocumentPackage("document.go", writer -> { + writer.writeDocs(String.format("%s is an interface which is used to bind" + + " a document type to its service client.", serviceSmithyDocumentInterface)); writer.openBlock("type $T interface {", "}", serviceSmithyDocumentInterface, () -> writer.write("$L()", IS_SMITHY_DOCUMENT_METHOD)) .write(""); - Symbol internalDocumentInterface = getInternalDocumentSymbol(DOCUMENT_INTERFACE_NAME); - Symbol smithyDocumentMarshaler = SymbolUtils.createValueSymbolBuilder("SmithyDocumentMarshaler", - SmithyGoDependency.SMITHY_DOCUMENT).build(); - Symbol smithyDocumentUnmarshaler = SymbolUtils.createValueSymbolBuilder("SmithyDocumentUnmarshaler", - SmithyGoDependency.SMITHY_DOCUMENT).build(); - + writer.writeDocs(String.format("%s is a JSON-like data model type that is protocol agnostic and is used" + + "to send open-content to a service.", internalDocumentInterface)); writer.openBlock("type $T interface {", "}", internalDocumentInterface, () -> { writer.write("$T", serviceSmithyDocumentInterface); writer.write("$T", smithyDocumentMarshaler); writer.write("$T", smithyDocumentUnmarshaler); }).write(""); + }); + writeInternalDocumentPackage("document_test.go", writer -> { writer.write("var _ $T = ($P)(nil)", serviceSmithyDocumentInterface, internalDocumentInterface); writer.write("var _ $T = ($P)(nil)", @@ -163,16 +255,65 @@ private void generateInternalDocumentInterface() { /** * Generates the internal document Go package for the service client. Delegates the logic for document marshaling * and unmarshalling types to the provided protocol generator using the given context. + *

+ * Generate a document marshaler type for marshaling documents to the service's protocol document format. + * + *

{@code
+     * type documentMarshaler struct {
+     *     value interface{}
+     * }
+     *
+     * func NewDocumentMarshaler(v interface{}) Interface {
+     *     // default or protocol implementation
+     * }
+     *
+     * func (m *documentMarshaler) UnmarshalSmithyDocument(v interface{}) error {
+     *     // implemented by protocol generator
+     * }
+     *
+     * func (m *documentUnmarshaler) MarshalSmithyDocument() ([]byte, error) {
+     *     // implemented by protocol generator
+     * }
+     * }
+ *

+ * Generate a document marshaler type for unmarshalling documents from the service's protocol response to a Go + * type. + * + *

{@code
+     * type documentUnmarshaler struct {
+     *     value interface{}
+     * }
+     * func NewDocumentUnmarshaler(v interface{}) Interface {
+     *     // default or protocol implementation
+     * }
+     *
+     * func (m *documentUnmarshaler) UnmarshalSmithyDocument(v interface{}) error {
+     *     // implemented by protocol generator
+     * }
+     *
+     * func (m *documentUnmarshaler) MarshalSmithyDocument() ([]byte, error) {
+     *     // implemented by protocol generator
+     * }
+     * }
+ *

+ * Generate {@code IsInterface} function which is used to assert whether a given document type + * is a valid service protocol document type implementation. + * + *

{@code
+     * func IsInterface(v Interface) bool {
+     *     // implementation
+     * }
+     * }
* * @param protocolGenerator the protocol generator. * @param context the protocol generator context. */ public void generateInternalDocumentTypes(ProtocolGenerator protocolGenerator, GenerationContext context) { - if (!hasDocumentShapes()) { + if (!this.hasDocumentShapes) { return; } - delegator.useFileWriter(getInternalDocumentFilePath("document.go"), getInternalDocumentPackage(), writer -> { + writeInternalDocumentPackage("document.go", writer -> { Symbol marshalerSymbol = getInternalDocumentSymbol("documentMarshaler", true); Symbol unmarshalerSymbol = getInternalDocumentSymbol("documentUnmarshaler", true); @@ -207,6 +348,8 @@ public void generateInternalDocumentTypes(ProtocolGenerator protocolGenerator, G Symbol documentInterfaceSymbol = getInternalDocumentSymbol(DOCUMENT_INTERFACE_NAME); + writer.writeDocs(String.format("%s creates a new document marshaler for the given input type", + INTERNAL_NEW_DOCUMENT_MARSHALER_FUNC)); writer.openBlock("func $L(v interface{}) $T {", "}", INTERNAL_NEW_DOCUMENT_MARSHALER_FUNC, documentInterfaceSymbol, () -> { protocolGenerator.generateNewDocumentMarshaler(context.toBuilder() @@ -214,6 +357,8 @@ public void generateInternalDocumentTypes(ProtocolGenerator protocolGenerator, G .build(), marshalerSymbol); }).write(""); + writer.writeDocs(String.format("%s creates a new document unmarshaler for the given service response", + INTERNAL_NEW_DOCUMENT_UNMARSHALER_FUNC)); writer.openBlock("func $L(v interface{}) $T {", "}", INTERNAL_NEW_DOCUMENT_UNMARSHALER_FUNC, documentInterfaceSymbol, () -> { protocolGenerator.generateNewDocumentUnmarshaler(context.toBuilder() @@ -221,6 +366,8 @@ public void generateInternalDocumentTypes(ProtocolGenerator protocolGenerator, G .build(), unmarshalerSymbol); }).write(""); + writer.writeDocs(String.format("%s returns whether the given Interface implementation is" + + " a valid client implementation", isDocumentInterface)); writer.openBlock("func $T(v Interface) (ok bool) {", "}", isDocumentInterface, () -> { writer.openBlock("defer func() {", "}()", () -> { writer.openBlock("if err := recover(); err != nil {", "}", () -> writer.write("ok = false")); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ShapeValueGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ShapeValueGenerator.java index 017c58cc1..5df9ba2ba 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ShapeValueGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ShapeValueGenerator.java @@ -17,6 +17,7 @@ package software.amazon.smithy.go.codegen; +import java.math.BigInteger; import java.util.List; import java.util.Map; import java.util.Optional; @@ -167,7 +168,7 @@ private void documentDeclShapeValue(GoWriter writer, MemberShape member, Node pa ProtocolDocumentGenerator.NEW_LAZY_DOCUMENT).build(); writer.writeInline("$T(", newMarshaler); - new DocumentValueNodeVisitor(writer).getDefault(params); + params.accept(new DocumentValueNodeVisitor(writer)); writer.writeInline(")"); } @@ -407,7 +408,7 @@ public Config build() { } } - private static final class DocumentValueNodeVisitor extends NodeVisitor.Default { + private static final class DocumentValueNodeVisitor implements NodeVisitor { private final GoWriter writer; private DocumentValueNodeVisitor(GoWriter writer) { @@ -418,7 +419,7 @@ private DocumentValueNodeVisitor(GoWriter writer) { public Void arrayNode(ArrayNode node) { writer.writeInline("[]interface{}{\n"); for (Node element : node.getElements()) { - getDefault(element); + element.accept(this); writer.writeInline(",\n"); } writer.writeInline("}"); @@ -444,7 +445,17 @@ public Void nullNode(NullNode node) { @Override public Void numberNode(NumberNode node) { if (node.isNaturalNumber()) { - writer.writeInline("$L", node.getValue()); + Number value = node.getValue(); + if (value instanceof BigInteger) { + writer.addUseImports(SmithyGoDependency.BIG); + writer.writeInline("func () *big.Int {\n" + + "\ti, ok := (&big.Int{}).SetString($S, 10)\n" + + "\tif !ok { panic(\"failed to parse string to integer: \" + $S) }\n" + + "\treturn i\n" + + "}()", value, value); + } else { + writer.writeInline("$L", node.getValue()); + } } else { Number value = node.getValue(); if (value instanceof Float) { @@ -468,7 +479,7 @@ public Void objectNode(ObjectNode node) { writer.writeInline("map[string]interface{}{\n"); node.getMembers().forEach((key, value) -> { writer.writeInline("$S: ", key.getValue()); - getDefault(value); + value.accept(this); writer.writeInline(",\n"); }); writer.writeInline("}"); @@ -480,32 +491,6 @@ public Void stringNode(StringNode node) { writer.writeInline("$S", node.getValue()); return null; } - - public Void getDefault(Node node) { - switch (node.getType()) { - case OBJECT: - objectNode(node.expectObjectNode()); - break; - case ARRAY: - arrayNode(node.expectArrayNode()); - break; - case STRING: - stringNode(node.expectStringNode()); - break; - case NUMBER: - numberNode(node.expectNumberNode()); - break; - case BOOLEAN: - booleanNode(node.expectBooleanNode()); - break; - case NULL: - nullNode(node.expectNullNode()); - break; - default: - throw new CodegenException("unknown node type: " + node.getType()); - } - return null; - } } /** diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/StructureGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/StructureGenerator.java index 9c10a888c..590558207 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/StructureGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/StructureGenerator.java @@ -108,6 +108,11 @@ public void renderStructure(Runnable runnable, boolean isInputStructure) { runnable.run(); + // At this moment there is no support for the concept of modeled document structure types. + // We embed the NoSerde type to prevent usage of the generated structure shapes from being used + // as document types themselves, or part of broader document-type structures. This avoids making backwards + // incompatible changes if the document type representation changes if it is later annotated as a modeled + // document type. This restriction may be relaxed later by removing this constraint. writer.write(""); writer.write("$L", ProtocolDocumentGenerator.NO_DOCUMENT_SERDE_TYPE_NAME); diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ProtocolGenerator.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ProtocolGenerator.java index 1758cdca5..612ffb871 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ProtocolGenerator.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ProtocolGenerator.java @@ -266,22 +266,122 @@ default Map getOperationErrors(GenerationContext context, Opera return HttpProtocolGeneratorUtils.getOperationErrors(context, operation); } + /** + * Generates the UnmarshalSmithyDocument function body of the service's internal documentMarshaler type. + *

+ * The document marshaler type is expected to handle user provided Go types and convert them to protocol documents. + *

+ * The default implementation will throw a {@code CodegenException} if not implemented. + * + *

{@code
+     * type documentMarshaler struct {
+     *     value interface{}
+     * }
+     *
+     * // ...
+     *
+     * func (m *documentMarshaler) UnmarshalSmithyDocument(v interface{}) error {
+     *      // Generated code from generateProtocolDocumentMarshalerUnmarshalDocument
+     * }
+     * }
+ * + * @param context the generation context. + */ default void generateProtocolDocumentMarshalerUnmarshalDocument(GenerationContext context) { - throw new CodegenException("document types not implemented for protocol"); + throw new CodegenException("document types not implemented for " + this.getProtocolName() + " protocol"); } + /** + * Generates the UnmarshalSmithyDocument function body of the service's internal documentMarshaler type. + *

+ * The document marshaler type is expected to handle user provided Go types and convert them to protocol documents. + *

+ * The default implementation will throw a {@code CodegenException} if not implemented. + * + *

{@code
+     * type documentMarshaler struct {
+     *     value interface{}
+     * }
+     *
+     * // ...
+     *
+     * func (m *documentMarshaler) MarshalSmithyDocument() ([]byte, error) {
+     *      // Generated code from generateProtocolDocumentMarshalerMarshalDocument
+     * }
+     * }
+ * + * @param context the generation context. + */ default void generateProtocolDocumentMarshalerMarshalDocument(GenerationContext context) { - throw new CodegenException("document types not implemented for protocol"); + throw new CodegenException("document types not implemented for " + this.getProtocolName() + " protocol"); } + /** + * Generates the UnmarshalSmithyDocument function body of the service's internal documentUnmarshaler type. + *

+ * The document unmarshaler type is expected to handle protocol documents received from the service and provide the + * ability to unmarshal or round-trip the document. + *

+ * The default implementation will throw a {@code CodegenException} if not implemented. + * + *

{@code
+     * type documentUnmarshaler struct {
+     *     value interface{}
+     * }
+     *
+     * // ...
+     *
+     * func (m *documentUnmarshaler) UnmarshalSmithyDocument(v interface{}) error {
+     *      // Generated code from generateProtocolDocumentUnmarshalerUnmarshalDocument
+     * }
+     * }
+ * + * @param context the generation context. + */ default void generateProtocolDocumentUnmarshalerUnmarshalDocument(GenerationContext context) { - throw new CodegenException("document types not implemented for protocol"); + throw new CodegenException("document types not implemented for " + this.getProtocolName() + " protocol"); } + /** + * Generates the MarshalSmithyDocument function body of the service's internal documentUnmarshaler type. + *

+ * The document unmarshaler type is expected to handle protocol documents received from the service and provide the + * ability to unmarshal or round-trip the document. + *

+ * The default implementation will throw a {@code CodegenException} if not implemented. + * + *

{@code
+     * type documentUnmarshaler struct {
+     *     value interface{}
+     * }
+     *
+     * // ...
+     *
+     * func (m *documentUnmarshaler) MarshalSmithyDocument() ([]byte, error) {
+     *      // Generated code from generateProtocolDocumentUnmarshalerMarshalDocument
+     * }
+     * }
+ * + * @param context the generation context. + */ default void generateProtocolDocumentUnmarshalerMarshalDocument(GenerationContext context) { - throw new CodegenException("document types not implemented for protocol"); + throw new CodegenException("document types not implemented for " + this.getProtocolName() + " protocol"); } + /** + * Generate the internal constructor function body for the service's internal documentMarshaler type. + * + *
{@code
+     * func NewDocumentMarshaler(v interface{}) Interface {
+     *     return &documentMarshaler{
+     *         value: v,
+     *     }
+     * }
+     * }
+ * + * @param context the generation context. + * @param marshalerSymbol the symbol for the {@code documentMarshaler} type. + */ default void generateNewDocumentMarshaler(GenerationContext context, Symbol marshalerSymbol) { GoWriter writer = context.getWriter().get(); writer.openBlock("return &$T{", "}", marshalerSymbol, () -> { @@ -289,6 +389,20 @@ default void generateNewDocumentMarshaler(GenerationContext context, Symbol mars }); } + /** + * Generate the internal constructor function body for the service's internal documentUnmarshaler type. + * + *
{@code
+     * func NewDocumentUnmarshaler(v interface{}) Interface {
+     *     return &documentUnmarshaler{
+     *         value: v,
+     *     }
+     * }
+     * }
+ * + * @param context the generation context. + * @param unmarshalerSymbol the symbol for the {@code documentUnmarshaler} type. + */ default void generateNewDocumentUnmarshaler(GenerationContext context, Symbol unmarshalerSymbol) { GoWriter writer = context.getWriter().get(); writer.openBlock("return &$T{", "}", unmarshalerSymbol, () -> { diff --git a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ProtocolUtils.java b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ProtocolUtils.java index e600b261e..bab20653b 100644 --- a/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ProtocolUtils.java +++ b/codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/integration/ProtocolUtils.java @@ -210,8 +210,4 @@ public static void writeSerDelegateFunction( lambda.accept(acceptVar); } - - public static void writeDocumentInterfaceCheck(GenerationContext context, GoWriter writer) { - - } } diff --git a/codegen/smithy-go-codegen/src/main/resources/software/amazon/smithy/go/codegen/document_doc.go.template b/codegen/smithy-go-codegen/src/main/resources/software/amazon/smithy/go/codegen/document_doc.go.template new file mode 100644 index 000000000..a9663a34f --- /dev/null +++ b/codegen/smithy-go-codegen/src/main/resources/software/amazon/smithy/go/codegen/document_doc.go.template @@ -0,0 +1,63 @@ +Package document implements encoding and decoding of open-content that has a JSON-like data model. +This data-model allows for UTF-8 strings, arbitrary precision numbers, booleans, nulls, a list of these values, and a +map of UTF-8 strings to these values. + +Interface defines the semantics for how a document type is marshalled and unmarshalled for requests and responses +for the service. To send a document as input to the service you use NewLazyDocument and pass it the Go type to be +sent to the service. NewLazyDocument returns a document Interface type that encodes the provided Go type during +the request serialization step after you have invoked an API client operation that uses the document type. + +The following examples show how you can create document types using basic Go types. + + NewLazyDocument(map[string]interface{}{ + "favoriteNumber": 42, + "fruits": []string{"apple", "orange"}, + "capitals": map[string]interface{}{ + "Washington": "Olympia", + "Oregon": "Salem", + }, + "skyIsBlue": true, + }) + + NewLazyDocument(3.14159) + + NewLazyDocument([]interface{"One", 2, 3, 3.5, "four"}) + + NewLazyDocument(true) + +Services can send document types as part of their API responses. To retrieve the content of a response document +you use the UnmarshalSmithyDocument method on the response document. When calling UnmarshalSmithyDocument you pass +a reference to the Go type that you want to unmarshal and map the response to. + +For example, if you expect to receive key/value map from the service response: + + var kv map[string]interface{} + if err := outputDocument.UnmarshalSmithyDocument(&kv); err != nil { + // handle error + } + +If a service can return one or more data-types in the response, you can use an empty interface and type switch to +dynamically handle the response type. + + var v interface{} + if err := outputDocument.UnmarshalSmithyDocument(&v); err != nil { + // handle error + } + + switch vv := v.(type) { + case map[string]interface{}: + // handle key/value map + case []interface{}: + // handle array of values + case bool: + // handle boolean + case document.Number: + // handle an arbitrary precision number + case string: + // handle string + default: + // handle unknown case + } + +The mapping of Go types to document types is covered in more depth in https://pkg.go.dev/github.com/aws/smithy-go/document +including more in depth examples that cover user-defined structure types. diff --git a/document/doc.go b/document/doc.go new file mode 100644 index 000000000..03055b7a1 --- /dev/null +++ b/document/doc.go @@ -0,0 +1,12 @@ +// Package document provides interface definitions and error types for document types. +// +// A document is a protocol-agnostic type which supports a JSON-like data-model. You can use this type to send +// UTF-8 strings, arbitrary precision numbers, booleans, nulls, a list of these values, and a map of UTF-8 +// strings to these values. +// +// API Clients expose document constructors in their respective client document packages which must be used to +// Marshal and Unmarshal Go types to and from their respective protocol representations. +// +// See the Marshaler and Unmarshaler type documentation for more details on how to Go types can be converted to and from +// document types. +package document diff --git a/document/document.go b/document/document.go index c2c1f3607..df7d60170 100644 --- a/document/document.go +++ b/document/document.go @@ -1,15 +1,78 @@ package document import ( + "fmt" + "math/big" "strconv" ) -type SmithyDocumentMarshaler interface { +// Marshaler is an interface for a type that marshals a document to its protocol-specific byte representation and +// returns the resulting bytes. A non-nil error will be returned if an error is encountered during marshaling. +// +// Marshal supports basic scalars (int,uint,float,bool,string), big.Int, and big.Float, maps, slices, and structs. +// Anonymous nested types are flattened based on Go anonymous type visibility. +// +// When defining struct types. the `document` struct tag can be used to control how the value will be +// marshaled into the resulting protocol document. +// +// // Field is ignored +// Field int `document:"-"` +// +// // Field object of key "myName" +// Field int `document:"myName"` +// +// // Field object key of key "myName", and +// // Field is omitted if the field is a zero value for the type. +// Field int `document:"myName,omitempty"` +// +// // Field object key of "Field", and +// // Field is omitted if the field is a zero value for the type. +// Field int `document:",omitempty"` +// +// All struct fields, including anonymous fields, are marshaled unless the +// any of the following conditions are meet. +// +// - the field is not exported +// - document field tag is "-" +// - document field tag specifies "omitempty", and is a zero value. +// +// Pointer and interfaces values are encoded as the value pointed to or +// contained in the interface. A nil value encodes as a null +// value unless `omitempty` struct tag is provided. +// +// Channel, complex, and function values are not encoded and will be skipped +// when walking the value to be marshaled. +// +// time.Time is not supported and will cause the Marshaler to return an error. These values should be represented +// by your application as a string or numerical representation. +// +// Errors that occur when marshaling will stop the marshaller, and return the error. +// +// Marshal cannot represent cyclic data structures and will not handle them. +// Passing cyclic structures to Marshal will result in an infinite recursion. +type Marshaler interface { MarshalSmithyDocument() ([]byte, error) } -type SmithyDocumentUnmarshaler interface { - UnmarshalSmithyDocument(interface{}) error +// Unmarshaler is an interface for a type that unmarshalls a document from its protocol-specific representation, and +// stores the result into the value pointed by v. If v is nil or not a pointer then InvalidUnmarshalError will be +// returned. +// +// Unmarshaler supports the same encodings produced by a document Marshaler. This includes support for the `document` +// struct field tag for controlling how struct fields are unmarshalled. +// +// Both generic interface{} and concrete types are valid unmarshal destination types. When unmarshalling a document +// into an empty interface the Unmarshaler will store one of these values: +// bool, for boolean values +// document.Number, for arbitrary-precision numbers (int64, float64, big.Int, big.Float) +// string, for string values +// []interface{}, for array values +// map[string]interface{}, for objects +// nil, for null values +// +// When unmarshalling, any error that occurs will halt the unmarshal and return the error. +type Unmarshaler interface { + UnmarshalSmithyDocument(v interface{}) error } type noSerde interface { @@ -47,6 +110,7 @@ func (n Number) intOfBitSize(bitSize int) (int64, error) { return strconv.ParseInt(string(n), 10, bitSize) } +// Uint64 returns the number as an uint64. func (n Number) Uint64() (uint64, error) { return n.uintOfBitSize(64) } @@ -69,3 +133,21 @@ func (n Number) Float64() (float64, error) { func (n Number) floatOfBitSize(bitSize int) (float64, error) { return strconv.ParseFloat(string(n), bitSize) } + +// BigFloat attempts to convert the number to a big.Float, returns an error if the operation fails. +func (n Number) BigFloat() (*big.Float, error) { + f, ok := (&big.Float{}).SetString(string(n)) + if !ok { + return nil, fmt.Errorf("failed to convert to big.Float") + } + return f, nil +} + +// BigInt attempts to convert the number to a big.Int, returns an error if the operation fails. +func (n Number) BigInt() (*big.Int, error) { + f, ok := (&big.Int{}).SetString(string(n), 10) + if !ok { + return nil, fmt.Errorf("failed to convert to big.Float") + } + return f, nil +} diff --git a/document/errors.go b/document/errors.go index da5b00673..f40168d3b 100644 --- a/document/errors.go +++ b/document/errors.go @@ -6,7 +6,8 @@ import ( ) // UnmarshalTypeError is an error type representing aa error -// unmarshalling a Smithy document to a Go value type. +// unmarshalling a Smithy document to a Go value type. This is different +// from UnmarshalError in that it does not wrap an underlying error type. type UnmarshalTypeError struct { Value string Type reflect.Type @@ -49,7 +50,7 @@ type UnmarshalError struct { Type reflect.Type } -// Unwrap returnbs the underlying unmarshalling error +// Unwrap returns the underlying unmarshalling error func (e *UnmarshalError) Unwrap() error { return e.Err } @@ -62,7 +63,7 @@ func (e *UnmarshalError) Error() string { } // An InvalidMarshalError is an error type representing an error -// occurring when marshaling a Go value type to an AttributeValue. +// occurring when marshaling a Go value type. type InvalidMarshalError struct { Message string } diff --git a/document/internal/serde/field.go b/document/internal/serde/field.go index b1ac016f9..64a9fdb33 100644 --- a/document/internal/serde/field.go +++ b/document/internal/serde/field.go @@ -7,6 +7,7 @@ import ( const tagKey = "document" +// Field is represents a struct field, tag, type, and index. type Field struct { Tag diff --git a/document/internal/serde/field_cache.go b/document/internal/serde/field_cache.go index 7815d6b7c..b00569e4f 100644 --- a/document/internal/serde/field_cache.go +++ b/document/internal/serde/field_cache.go @@ -23,15 +23,18 @@ func (c *fieldCacher) LoadOrStore(t interface{}, fs *CachedFields) (*CachedField return v.(*CachedFields), ok } +// CachedFields is a cache entry for a type's fields. type CachedFields struct { fields []Field fieldsByName map[string]int } +// All returns all the fields for the cached type. func (f *CachedFields) All() []Field { return f.fields } +// FieldByName retrieves a field by name. func (f *CachedFields) FieldByName(name string) (Field, bool) { if i, ok := f.fieldsByName[name]; ok { return f.fields[i], ok diff --git a/document/internal/serde/field_test.go b/document/internal/serde/field_test.go index 1b28f02df..fc8b956fd 100644 --- a/document/internal/serde/field_test.go +++ b/document/internal/serde/field_test.go @@ -22,7 +22,7 @@ type unionComplex struct { } type unionTagged struct { - A int `json:"A"` + A int `document:"A"` } type unionTaggedComplex struct { diff --git a/document/internal/serde/reflect.go b/document/internal/serde/reflect.go index 1a7e756ba..165145cda 100644 --- a/document/internal/serde/reflect.go +++ b/document/internal/serde/reflect.go @@ -7,6 +7,8 @@ import ( "time" ) +// ReflectTypeOf is a structure containing various reflect.Type members that are useful +// to document Marshaler or Unmarshaler implementations. var ReflectTypeOf = struct { BigFloat reflect.Type BigInt reflect.Type diff --git a/document/internal/serde/serde.go b/document/internal/serde/serde.go index 80ef5a9b6..c2bdbd38b 100644 --- a/document/internal/serde/serde.go +++ b/document/internal/serde/serde.go @@ -47,6 +47,7 @@ func Indirect(v reflect.Value, decodingNull bool) reflect.Value { return v } +// PtrToValue given the input value will dereference pointers and returning the element pointed to. func PtrToValue(in interface{}) interface{} { v := reflect.ValueOf(in) if v.Kind() == reflect.Ptr { @@ -61,6 +62,7 @@ func PtrToValue(in interface{}) interface{} { return v.Interface() } +// IsZeroValue returns whether v is the zero-value for its type. func IsZeroValue(v reflect.Value) bool { switch v.Kind() { case reflect.Invalid: @@ -85,6 +87,7 @@ func IsZeroValue(v reflect.Value) bool { return false } +// ValueElem walks interface and pointer types and returns the underlying element. func ValueElem(v reflect.Value) reflect.Value { switch v.Kind() { case reflect.Interface, reflect.Ptr: diff --git a/document/internal/serde/tags.go b/document/internal/serde/tags.go index 675a1acf8..2097f12b4 100644 --- a/document/internal/serde/tags.go +++ b/document/internal/serde/tags.go @@ -4,6 +4,7 @@ import ( "strings" ) +// Tag represents the `document` struct field tag and associated options type Tag struct { Name string Ignore bool diff --git a/document/json/decoder.go b/document/json/decoder.go index fb5cf8fdd..366406c7a 100644 --- a/document/json/decoder.go +++ b/document/json/decoder.go @@ -10,24 +10,39 @@ import ( "github.com/aws/smithy-go/document/internal/serde" ) +// DecoderOptions is the set of options that can be configured for a Decoder. type DecoderOptions struct{} +// Decoder is a Smithy document decoder for JSON based protocols. type Decoder struct { options DecoderOptions } -func (d *Decoder) DecodeJSONInterface(json interface{}, value interface{}) error { - if document.IsNoSerde(value) { - return fmt.Errorf("unsupported type: %T", value) +// DecodeJSONInterface decodes the supported JSON input types and stores the result in the value pointed by toValue. +// +// If toValue is not a compatible type, or an error occurs while decoding DecodeJSONInterface will return an error. +// +// The supported input JSON types are: +// bool -> JSON boolean +// float64 -> JSON number +// json.Number -> JSON number +// string -> JSON string +// []interface{} -> JSON array +// map[string]interface{} -> JSON object +// nil -> JSON null +// +func (d *Decoder) DecodeJSONInterface(input interface{}, toValue interface{}) error { + if document.IsNoSerde(toValue) { + return fmt.Errorf("unsupported type: %T", toValue) } - v := reflect.ValueOf(value) + v := reflect.ValueOf(toValue) if v.Kind() != reflect.Ptr || v.IsNil() || !v.IsValid() { - return &document.InvalidUnmarshalError{Type: reflect.TypeOf(value)} + return &document.InvalidUnmarshalError{Type: reflect.TypeOf(toValue)} } - return d.decode(json, v, serde.Tag{}) + return d.decode(input, v, serde.Tag{}) } func (d *Decoder) decode(jv interface{}, rv reflect.Value, tag serde.Tag) error { @@ -168,9 +183,6 @@ func (d *Decoder) decodeJSONNumber(tv json.Number, rv reflect.Value) error { func (d *Decoder) decodeJSONFloat64(tv float64, rv reflect.Value) error { switch rv.Kind() { case reflect.Interface: - if rv.NumMethod() != 0 { - return &document.UnmarshalTypeError{Value: "number", Type: rv.Type()} - } rv.Set(reflect.ValueOf(tv)) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: i, accuracy := big.NewFloat(tv).Int64() @@ -323,6 +335,10 @@ func (d *Decoder) decodeJSONObject(tv map[string]interface{}, rv reflect.Value) } func (d *Decoder) unsupportedType(jv interface{}, rv reflect.Value) error { + if rv.Kind() == reflect.Interface && rv.NumMethod() != 0 { + return &document.UnmarshalTypeError{Value: "non-empty interface", Type: rv.Type()} + } + if rv.Type().ConvertibleTo(serde.ReflectTypeOf.Time) { return &document.UnmarshalTypeError{ Type: rv.Type(), diff --git a/document/json/decoder_test.go b/document/json/decoder_test.go index 58810d9f7..9387fc9d6 100644 --- a/document/json/decoder_test.go +++ b/document/json/decoder_test.go @@ -1,143 +1,16 @@ package json_test import ( - "bytes" - stdjson "encoding/json" - "math" "math/big" - "strconv" "testing" "time" "github.com/aws/smithy-go/document" "github.com/aws/smithy-go/document/internal/serde" "github.com/aws/smithy-go/document/json" - "github.com/aws/smithy-go/ptr" "github.com/google/go-cmp/cmp" ) -type StructA struct { - FieldName string - FieldPtrName *string - - FieldRename string `document:"field_rename"` - FieldIgnored string `document:"-"` - - FieldOmitEmpty string `document:",omitempty"` - FieldPtrOmitEmpty *string `document:",omitempty"` - - FieldRenameOmitEmpty string `document:"field_rename_omit_empty,omitempty"` - - FieldNestedStruct *StructA `document:"field_nested_struct"` - FieldNestedStructOmitEmpty *StructA `document:"field_nested_struct_omit_empty,omitempty"` - - fieldUnexported string - - StructB -} - -type StructB struct { - FieldName string `document:"field_name"` -} - -type testCase struct { - decoderOptions json.DecoderOptions - encoderOptions json.EncoderOptions - disableJSONNumber bool - json []byte - actual, want interface{} - wantErr bool -} - -var sharedStringTests = map[string]testCase{ - "string": { - json: []byte(`"foo"`), - actual: func() interface{} { - var v string - return &v - }(), - want: "foo", - }, - "interface{}": { - json: []byte(`"foo"`), - actual: func() interface{} { - var v interface{} - return &v - }(), - want: "foo", - }, -} - -var sharedObjectTests = map[string]testCase{ - "null for pointer type": { - json: []byte(`null`), - actual: func() interface{} { - var v *StructA - return &v - }(), - }, - "zero value": { - json: []byte(`{ - "FieldName": "", - "FieldPtrName": null, - "field_name": "", - "field_nested_struct": null, - "field_rename": "" -}`), - actual: func() interface{} { - var v StructA - return &v - }(), - want: StructA{}, - }, - "filled json structure": { - json: []byte(`{ - "FieldName": "a", - "FieldPtrName": "b", - "field_rename": "c", - "FieldOmitEmpty": "d", - "FieldPtrOmitEmpty": "e", - "field_rename_omit_empty": "f", - "field_nested_struct": { - "FieldName": "a", - "FieldPtrName": null, - "field_name": "", - "field_nested_struct": null, - "field_rename": "" - }, - "field_nested_struct_omit_empty": { - "FieldName": "a", - "FieldPtrName": null, - "field_name": "", - "field_nested_struct": null, - "field_rename": "" - }, - "field_name": "A" -}`), - actual: func() interface{} { - var v *StructA - return &v - }(), - want: &StructA{ - FieldName: "a", - FieldPtrName: ptr.String("b"), - FieldRename: "c", - FieldOmitEmpty: "d", - FieldPtrOmitEmpty: ptr.String("e"), - FieldRenameOmitEmpty: "f", - FieldNestedStruct: &StructA{ - FieldName: "a", - }, - FieldNestedStructOmitEmpty: &StructA{ - FieldName: "a", - }, - StructB: StructB{ - FieldName: "A", - }, - }, - }, -} - var decodeArrayTestCases = map[string]testCase{ "array not enough capacity": { json: []byte(`["foo", "bar", "baz"]`), @@ -149,380 +22,44 @@ var decodeArrayTestCases = map[string]testCase{ }, } -var sharedArrayTestCases = map[string]testCase{ - "slice": { - json: []byte(`["foo", "bar", "baz"]`), - actual: func() interface{} { - var v []string - return &v - }(), - want: []string{"foo", "bar", "baz"}, - }, - "array": { - json: []byte(`["foo", "bar", "baz"]`), - actual: func() interface{} { - var v [3]string - return &v - }(), - want: [3]string{"foo", "bar", "baz"}, - }, - - "interface{}": { - json: []byte(`["foo", "bar", "baz"]`), - actual: func() interface{} { - var v interface{} - return &v - }(), - want: []interface{}{"foo", "bar", "baz"}, - }, -} - -var sharedNumberTestCases = map[string]testCase{ - "json.Number to interface{}": { - json: []byte(`3.14159`), - actual: func() interface{} { - var v interface{} - return &v - }(), - want: ptrNumber("3.14159"), - }, - - "json float64 to interface{}": { - json: []byte(`3.14159`), - actual: func() interface{} { - var v interface{} - return &v - }(), - want: ptr.Float64(3.14159), - disableJSONNumber: true, - }, - - "json.Number to document.Number": { - json: []byte(`3.14159`), - actual: func() interface{} { - var v document.Number - return &v - }(), - want: document.Number("3.14159"), - }, - - "json.Number to *document.Number": { - json: []byte(`3.14159`), - actual: func() interface{} { - var v *document.Number - return &v - }(), - want: document.Number("3.14159"), - }, - - /* - int, int16, int32, int64 - */ - "json.Number to int": { - json: []byte(`2147483647`), - actual: func() interface{} { - var x int - return &x - }(), - want: ptr.Int(2147483647), - }, - "json float64 to int": { - json: []byte(`2147483647`), - actual: func() interface{} { - var x int - return &x - }(), - want: ptr.Int(2147483647), - disableJSONNumber: true, - }, - "json.Number to int8": { - json: []byte(`127`), - actual: func() interface{} { - var x int8 - return &x - }(), - want: ptr.Int8(127), - }, - "json float64 to int8": { - json: []byte(`127`), - actual: func() interface{} { - var x int8 - return &x - }(), - want: ptr.Int8(127), - disableJSONNumber: true, - }, - "json.Number to int16": { - json: []byte(`32767`), - actual: func() interface{} { - var x int16 - return &x - }(), - want: ptr.Int16(32767), - }, - "json float64 to int16": { - json: []byte(`32767`), - actual: func() interface{} { - var x int16 - return &x - }(), - want: ptr.Int16(32767), - disableJSONNumber: true, - }, - "json.Number to int32": { - json: []byte(`2147483647`), - actual: func() interface{} { - var x int32 - return &x - }(), - want: ptr.Int32(2147483647), - }, - "json float64 to int32": { - json: []byte(`2147483647`), - actual: func() interface{} { - var x int32 - return &x - }(), - want: ptr.Int32(2147483647), - disableJSONNumber: true, - }, - "json.Number to int64": { - json: []byte("9223372036854775807"), - actual: func() interface{} { - var x int64 - return &x - }(), - want: ptr.Int64(9223372036854775807), - }, - "json float64 to int64": { - json: []byte("2147483648"), - actual: func() interface{} { - var x int64 - return &x - }(), - want: ptr.Int64(2147483648), - disableJSONNumber: true, - }, - - /* - uint, uint16, uint32, uint64 - */ - "json.Number to uint": { - json: []byte(`4294967295`), - actual: func() interface{} { - var x uint - return &x - }(), - want: ptr.Uint(4294967295), - }, - "json float64 to uint": { - json: []byte(`4294967295`), - actual: func() interface{} { - var x uint - return &x - }(), - want: ptr.Uint(4294967295), - disableJSONNumber: true, - }, - "json.Number to uint8": { - json: []byte(`255`), - actual: func() interface{} { - var x uint8 - return &x - }(), - want: ptr.Uint8(255), - }, - "json float64 to uint8": { - json: []byte(`255`), - actual: func() interface{} { - var x uint8 - return &x - }(), - want: ptr.Uint8(255), - disableJSONNumber: true, - }, - "json.Number to uint16": { - json: []byte(`65535`), - actual: func() interface{} { - var x uint16 - return &x - }(), - want: ptr.Uint16(65535), - }, - "json float64 to uint16": { - json: []byte(`65535`), - actual: func() interface{} { - var x uint16 - return &x - }(), - want: ptr.Uint16(65535), - disableJSONNumber: true, - }, - "json.Number to uint32": { - json: []byte(`4294967295`), - actual: func() interface{} { - var x uint32 - return &x - }(), - want: ptr.Uint32(4294967295), - }, - "json float64 to uint32": { - json: []byte(`4294967295`), - actual: func() interface{} { - var x uint32 - return &x - }(), - want: ptr.Uint32(4294967295), - disableJSONNumber: true, - }, - "json.Number to uint64": { - json: []byte("18446744073709551615"), - actual: func() interface{} { - var x uint64 - return &x - }(), - want: ptr.Uint64(18446744073709551615), - }, - "json float64 to uint64": { - json: []byte("4294967295"), - actual: func() interface{} { - var x uint64 - return &x - }(), - want: ptr.Uint64(4294967295), - disableJSONNumber: true, - }, - - /* - float32, float64 - */ - "json.Number to float32": { - json: []byte(strconv.FormatFloat(math.MaxFloat32, 'e', -1, 32)), - actual: func() interface{} { - var x float32 - return &x - }(), - want: ptr.Float32(math.MaxFloat32), - }, - "json float64 to float32": { - json: []byte(strconv.FormatFloat(3.14159, 'e', -1, 32)), - actual: func() interface{} { - var x float32 - return &x - }(), - want: ptr.Float32(3.14159), - disableJSONNumber: true, - }, - "json.Number to float64": { - json: []byte(strconv.FormatFloat(math.MaxFloat64, 'e', -1, 64)), - actual: func() interface{} { - var x float64 - return &x - }(), - want: ptr.Float64(math.MaxFloat64), - }, - "json float64 to float64": { - json: []byte(strconv.FormatFloat(3.14159, 'e', -1, 64)), - actual: func() interface{} { - var x float64 - return &x - }(), - want: ptr.Float64(3.14159), - disableJSONNumber: true, - }, - - /* - Arbitrary Number Sizes - */ - "json.Number to big.Float": { - json: []byte(strconv.FormatFloat(math.MaxFloat64, 'e', -1, 64)), - actual: func() interface{} { - var x big.Float - return &x - }(), - want: func() *big.Float { - return big.NewFloat(math.MaxFloat64) - }(), - }, - "float64 to big.Float": { - json: []byte(strconv.FormatFloat(math.MaxFloat64, 'e', -1, 64)), - actual: func() interface{} { - var x big.Float - return &x - }(), - want: func() *big.Float { - return big.NewFloat(math.MaxFloat64) - }(), - disableJSONNumber: true, - }, - "json.Number to big.Int": { - json: []byte(strconv.FormatInt(math.MaxInt64, 10)), - actual: func() interface{} { - var x big.Int - return &x - }(), - want: func() *big.Int { - return big.NewInt(math.MaxInt64) - }(), - }, - "float64 to big.Int": { - json: []byte(strconv.FormatInt(math.MaxInt32, 10)), - actual: func() interface{} { - var x big.Int - return &x - }(), - want: func() *big.Int { - return big.NewInt(math.MaxInt32) - }(), - disableJSONNumber: true, - }, -} - -func MustJSONUnmarshal(v []byte, useJSONNumber bool) interface{} { - var jv interface{} - decoder := stdjson.NewDecoder(bytes.NewReader(v)) - if useJSONNumber { - decoder.UseNumber() - } - if err := decoder.Decode(&jv); err != nil { - panic(err) - } - return jv -} - -func ptrNumber(number document.Number) *document.Number { - return &number -} - func TestDecoder_DecodeJSONInterface(t *testing.T) { - t.Run("Object", func(t *testing.T) { - for name, tt := range sharedObjectTests { - t.Run(name, func(t *testing.T) { - testDecodeJSONInterface(t, tt) - }) - } - }) - t.Run("Array", func(t *testing.T) { - for name, tt := range sharedArrayTestCases { - t.Run(name, func(t *testing.T) { - testDecodeJSONInterface(t, tt) - }) - } - }) - t.Run("Number", func(t *testing.T) { - for name, tt := range sharedNumberTestCases { - t.Run(name, func(t *testing.T) { - testDecodeJSONInterface(t, tt) - }) - } + t.Run("Shared", func(t *testing.T) { + t.Run("Object", func(t *testing.T) { + for name, tt := range sharedObjectTests { + t.Run(name, func(t *testing.T) { + testDecodeJSONInterface(t, tt) + }) + } + }) + t.Run("Array", func(t *testing.T) { + for name, tt := range sharedArrayTestCases { + t.Run(name, func(t *testing.T) { + testDecodeJSONInterface(t, tt) + }) + } + }) + t.Run("Number", func(t *testing.T) { + for name, tt := range sharedNumberTestCases { + t.Run(name, func(t *testing.T) { + testDecodeJSONInterface(t, tt) + }) + } + }) + t.Run("String", func(t *testing.T) { + for name, tt := range sharedStringTests { + t.Run(name, func(t *testing.T) { + testDecodeJSONInterface(t, tt) + }) + } + }) }) - t.Run("String", func(t *testing.T) { - for name, tt := range sharedStringTests { + for name, tt := range decodeArrayTestCases { + t.Run("Array", func(t *testing.T) { t.Run(name, func(t *testing.T) { testDecodeJSONInterface(t, tt) }) - } - }) + }) + } } func TestNoSerde(t *testing.T) { @@ -586,6 +123,16 @@ func TestNewDecoderUnsupportedTypes(t *testing.T) { return &t }(), }, + { + input: []byte(`{}`), + value: func() interface{} { + type def interface { + String() + } + var i def + return &i + }(), + }, } decoder := json.NewDecoder() diff --git a/document/json/doc.go b/document/json/doc.go new file mode 100644 index 000000000..cc5c04c34 --- /dev/null +++ b/document/json/doc.go @@ -0,0 +1,8 @@ +// Package json provides a document Encoder and Decoder implementation that is used to implement Smithy document types +// for JSON based protocols. The Encoder and Decoder implement the document.Marshaler and document.Unmarshaler +// interfaces respectively. +// +// This package handles protocol specific implementation details about documents, and can not be used to construct +// a document type for a service client. To construct a document type see each service clients respective document +// package and NewLazyDocument function. +package json diff --git a/document/json/encoder.go b/document/json/encoder.go index a1515b221..860f899ab 100644 --- a/document/json/encoder.go +++ b/document/json/encoder.go @@ -10,12 +10,15 @@ import ( smithyjson "github.com/aws/smithy-go/encoding/json" ) +// EncoderOptions is the set of options that can be configured for an Encoder. type EncoderOptions struct{} +// Encoder is a Smithy document decoder for JSON based protocols. type Encoder struct { options EncoderOptions } +// Encode returns the JSON encoding of v. func (e *Encoder) Encode(v interface{}) ([]byte, error) { if document.IsNoSerde(v) { return nil, fmt.Errorf("unsupported type: %v", v) @@ -36,21 +39,30 @@ func (e *Encoder) Encode(v interface{}) ([]byte, error) { return encodedBytes, nil } +// valueProvider is an interface for retrieving a JSON Value type used for encoding. +type valueProvider interface { + GetValue() smithyjson.Value +} + +// jsonValueProvider is a valueProvider that returns the JSON value encoder as is. type jsonValueProvider smithyjson.Value func (p jsonValueProvider) GetValue() smithyjson.Value { return smithyjson.Value(p) } -type valueProvider interface { - GetValue() smithyjson.Value -} - +// jsonObjectKeyProvider is a valueProvider that returns a JSON value type for encoding a value for the given JSON object +// key. type jsonObjectKeyProvider struct { Object *smithyjson.Object Key string } +func (p jsonObjectKeyProvider) GetValue() smithyjson.Value { + return p.Object.Key(p.Key) +} + +// jsonArrayProvider is a valueProvider that returns a JSON value type for encoding a value in the given JSON array. type jsonArrayProvider struct { Array *smithyjson.Array } @@ -59,10 +71,6 @@ func (p jsonArrayProvider) GetValue() smithyjson.Value { return p.Array.Value() } -func (p jsonObjectKeyProvider) GetValue() smithyjson.Value { - return p.Object.Key(p.Key) -} - func (e *Encoder) encode(vp valueProvider, rv reflect.Value, tag serde.Tag) error { // Zero values are serialized as null, or skipped if omitEmpty. if serde.IsZeroValue(rv) { diff --git a/document/json/json.go b/document/json/json.go index 72f3e1a62..ec4d29260 100644 --- a/document/json/json.go +++ b/document/json/json.go @@ -1,5 +1,6 @@ package json +// NewEncoder returns an Encoder for serializing Smithy documents for JSON based protocols. func NewEncoder(optFns ...func(options *EncoderOptions)) *Encoder { o := EncoderOptions{} @@ -12,6 +13,7 @@ func NewEncoder(optFns ...func(options *EncoderOptions)) *Encoder { } } +// NewDecoder returns a Decoder for deserializing Smithy documents for JSON based protocols. func NewDecoder(optFns ...func(*DecoderOptions)) *Decoder { o := DecoderOptions{} diff --git a/document/json/shared_test.go b/document/json/shared_test.go new file mode 100644 index 000000000..fd2fe1e3c --- /dev/null +++ b/document/json/shared_test.go @@ -0,0 +1,481 @@ +package json_test + +import ( + "bytes" + json2 "encoding/json" + "math" + "math/big" + "strconv" + + "github.com/aws/smithy-go/document" + "github.com/aws/smithy-go/document/json" + "github.com/aws/smithy-go/ptr" +) + +type StructA struct { + FieldName string + FieldPtrName *string + + FieldRename string `document:"field_rename"` + FieldIgnored string `document:"-"` + + FieldOmitEmpty string `document:",omitempty"` + FieldPtrOmitEmpty *string `document:",omitempty"` + + FieldRenameOmitEmpty string `document:"field_rename_omit_empty,omitempty"` + + FieldNestedStruct *StructA `document:"field_nested_struct"` + FieldNestedStructOmitEmpty *StructA `document:"field_nested_struct_omit_empty,omitempty"` + + fieldUnexported string + + StructB +} + +type StructB struct { + FieldName string `document:"field_name"` +} + +type testCase struct { + decoderOptions json.DecoderOptions + encoderOptions json.EncoderOptions + disableJSONNumber bool + json []byte + actual, want interface{} + wantErr bool +} + +var sharedStringTests = map[string]testCase{ + "string": { + json: []byte(`"foo"`), + actual: func() interface{} { + var v string + return &v + }(), + want: "foo", + }, + "interface{}": { + json: []byte(`"foo"`), + actual: func() interface{} { + var v interface{} + return &v + }(), + want: "foo", + }, +} + +var sharedObjectTests = map[string]testCase{ + "null for pointer type": { + json: []byte(`null`), + actual: func() interface{} { + var v *StructA + return &v + }(), + }, + "zero value": { + json: []byte(`{ + "FieldName": "", + "FieldPtrName": null, + "field_name": "", + "field_nested_struct": null, + "field_rename": "" +}`), + actual: func() interface{} { + var v StructA + return &v + }(), + want: StructA{}, + }, + "filled json structure": { + json: []byte(`{ + "FieldName": "a", + "FieldPtrName": "b", + "field_rename": "c", + "FieldOmitEmpty": "d", + "FieldPtrOmitEmpty": "e", + "field_rename_omit_empty": "f", + "field_nested_struct": { + "FieldName": "a", + "FieldPtrName": null, + "field_name": "", + "field_nested_struct": null, + "field_rename": "" + }, + "field_nested_struct_omit_empty": { + "FieldName": "a", + "FieldPtrName": null, + "field_name": "", + "field_nested_struct": null, + "field_rename": "" + }, + "field_name": "A" +}`), + actual: func() interface{} { + var v *StructA + return &v + }(), + want: &StructA{ + FieldName: "a", + FieldPtrName: ptr.String("b"), + FieldRename: "c", + FieldOmitEmpty: "d", + FieldPtrOmitEmpty: ptr.String("e"), + FieldRenameOmitEmpty: "f", + FieldNestedStruct: &StructA{ + FieldName: "a", + }, + FieldNestedStructOmitEmpty: &StructA{ + FieldName: "a", + }, + StructB: StructB{ + FieldName: "A", + }, + }, + }, +} + +var sharedArrayTestCases = map[string]testCase{ + "slice": { + json: []byte(`["foo", "bar", "baz"]`), + actual: func() interface{} { + var v []string + return &v + }(), + want: []string{"foo", "bar", "baz"}, + }, + "array": { + json: []byte(`["foo", "bar", "baz"]`), + actual: func() interface{} { + var v [3]string + return &v + }(), + want: [3]string{"foo", "bar", "baz"}, + }, + + "interface{}": { + json: []byte(`["foo", "bar", "baz"]`), + actual: func() interface{} { + var v interface{} + return &v + }(), + want: []interface{}{"foo", "bar", "baz"}, + }, +} + +var sharedNumberTestCases = map[string]testCase{ + "json.Number to interface{}": { + json: []byte(`3.14159`), + actual: func() interface{} { + var v interface{} + return &v + }(), + want: ptrNumber("3.14159"), + }, + + "json float64 to interface{}": { + json: []byte(`3.14159`), + actual: func() interface{} { + var v interface{} + return &v + }(), + want: ptr.Float64(3.14159), + disableJSONNumber: true, + }, + + "json.Number to document.Number": { + json: []byte(`3.14159`), + actual: func() interface{} { + var v document.Number + return &v + }(), + want: document.Number("3.14159"), + }, + + "json.Number to *document.Number": { + json: []byte(`3.14159`), + actual: func() interface{} { + var v *document.Number + return &v + }(), + want: document.Number("3.14159"), + }, + + /* + int, int16, int32, int64 + */ + "json.Number to int": { + json: []byte(`2147483647`), + actual: func() interface{} { + var x int + return &x + }(), + want: ptr.Int(2147483647), + }, + "json float64 to int": { + json: []byte(`2147483647`), + actual: func() interface{} { + var x int + return &x + }(), + want: ptr.Int(2147483647), + disableJSONNumber: true, + }, + "json.Number to int8": { + json: []byte(`127`), + actual: func() interface{} { + var x int8 + return &x + }(), + want: ptr.Int8(127), + }, + "json float64 to int8": { + json: []byte(`127`), + actual: func() interface{} { + var x int8 + return &x + }(), + want: ptr.Int8(127), + disableJSONNumber: true, + }, + "json.Number to int16": { + json: []byte(`32767`), + actual: func() interface{} { + var x int16 + return &x + }(), + want: ptr.Int16(32767), + }, + "json float64 to int16": { + json: []byte(`32767`), + actual: func() interface{} { + var x int16 + return &x + }(), + want: ptr.Int16(32767), + disableJSONNumber: true, + }, + "json.Number to int32": { + json: []byte(`2147483647`), + actual: func() interface{} { + var x int32 + return &x + }(), + want: ptr.Int32(2147483647), + }, + "json float64 to int32": { + json: []byte(`2147483647`), + actual: func() interface{} { + var x int32 + return &x + }(), + want: ptr.Int32(2147483647), + disableJSONNumber: true, + }, + "json.Number to int64": { + json: []byte("9223372036854775807"), + actual: func() interface{} { + var x int64 + return &x + }(), + want: ptr.Int64(9223372036854775807), + }, + "json float64 to int64": { + json: []byte("2147483648"), + actual: func() interface{} { + var x int64 + return &x + }(), + want: ptr.Int64(2147483648), + disableJSONNumber: true, + }, + + /* + uint, uint16, uint32, uint64 + */ + "json.Number to uint": { + json: []byte(`4294967295`), + actual: func() interface{} { + var x uint + return &x + }(), + want: ptr.Uint(4294967295), + }, + "json float64 to uint": { + json: []byte(`4294967295`), + actual: func() interface{} { + var x uint + return &x + }(), + want: ptr.Uint(4294967295), + disableJSONNumber: true, + }, + "json.Number to uint8": { + json: []byte(`255`), + actual: func() interface{} { + var x uint8 + return &x + }(), + want: ptr.Uint8(255), + }, + "json float64 to uint8": { + json: []byte(`255`), + actual: func() interface{} { + var x uint8 + return &x + }(), + want: ptr.Uint8(255), + disableJSONNumber: true, + }, + "json.Number to uint16": { + json: []byte(`65535`), + actual: func() interface{} { + var x uint16 + return &x + }(), + want: ptr.Uint16(65535), + }, + "json float64 to uint16": { + json: []byte(`65535`), + actual: func() interface{} { + var x uint16 + return &x + }(), + want: ptr.Uint16(65535), + disableJSONNumber: true, + }, + "json.Number to uint32": { + json: []byte(`4294967295`), + actual: func() interface{} { + var x uint32 + return &x + }(), + want: ptr.Uint32(4294967295), + }, + "json float64 to uint32": { + json: []byte(`4294967295`), + actual: func() interface{} { + var x uint32 + return &x + }(), + want: ptr.Uint32(4294967295), + disableJSONNumber: true, + }, + "json.Number to uint64": { + json: []byte("18446744073709551615"), + actual: func() interface{} { + var x uint64 + return &x + }(), + want: ptr.Uint64(18446744073709551615), + }, + "json float64 to uint64": { + json: []byte("4294967295"), + actual: func() interface{} { + var x uint64 + return &x + }(), + want: ptr.Uint64(4294967295), + disableJSONNumber: true, + }, + + /* + float32, float64 + */ + "json.Number to float32": { + json: []byte(strconv.FormatFloat(math.MaxFloat32, 'e', -1, 32)), + actual: func() interface{} { + var x float32 + return &x + }(), + want: ptr.Float32(math.MaxFloat32), + }, + "json float64 to float32": { + json: []byte(strconv.FormatFloat(3.14159, 'e', -1, 32)), + actual: func() interface{} { + var x float32 + return &x + }(), + want: ptr.Float32(3.14159), + disableJSONNumber: true, + }, + "json.Number to float64": { + json: []byte(strconv.FormatFloat(math.MaxFloat64, 'e', -1, 64)), + actual: func() interface{} { + var x float64 + return &x + }(), + want: ptr.Float64(math.MaxFloat64), + }, + "json float64 to float64": { + json: []byte(strconv.FormatFloat(3.14159, 'e', -1, 64)), + actual: func() interface{} { + var x float64 + return &x + }(), + want: ptr.Float64(3.14159), + disableJSONNumber: true, + }, + + /* + Arbitrary Number Sizes + */ + "json.Number to big.Float": { + json: []byte(strconv.FormatFloat(math.MaxFloat64, 'e', -1, 64)), + actual: func() interface{} { + var x big.Float + return &x + }(), + want: func() *big.Float { + return big.NewFloat(math.MaxFloat64) + }(), + }, + "float64 to big.Float": { + json: []byte(strconv.FormatFloat(math.MaxFloat64, 'e', -1, 64)), + actual: func() interface{} { + var x big.Float + return &x + }(), + want: func() *big.Float { + return big.NewFloat(math.MaxFloat64) + }(), + disableJSONNumber: true, + }, + "json.Number to big.Int": { + json: []byte(strconv.FormatInt(math.MaxInt64, 10)), + actual: func() interface{} { + var x big.Int + return &x + }(), + want: func() *big.Int { + return big.NewInt(math.MaxInt64) + }(), + }, + "float64 to big.Int": { + json: []byte(strconv.FormatInt(math.MaxInt32, 10)), + actual: func() interface{} { + var x big.Int + return &x + }(), + want: func() *big.Int { + return big.NewInt(math.MaxInt32) + }(), + disableJSONNumber: true, + }, +} + +func ptrNumber(number document.Number) *document.Number { + return &number +} + +func MustJSONUnmarshal(v []byte, useJSONNumber bool) interface{} { + var jv interface{} + decoder := json2.NewDecoder(bytes.NewReader(v)) + if useJSONNumber { + decoder.UseNumber() + } + if err := decoder.Decode(&jv); err != nil { + panic(err) + } + return jv +} + diff --git a/testing/struct.go b/testing/struct.go index 3f2b30992..080ee397d 100644 --- a/testing/struct.go +++ b/testing/struct.go @@ -42,8 +42,8 @@ func CompareValues(expect, actual interface{}, opts ...cmp.Option) error { } type documentInterface interface { - document.SmithyDocumentMarshaler - document.SmithyDocumentUnmarshaler + document.Marshaler + document.Unmarshaler } func compareDocumentTypes(x documentInterface, y documentInterface) bool {