diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CommandGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CommandGenerator.java index f24077fc3bc..31bedf7f4e8 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CommandGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CommandGenerator.java @@ -42,6 +42,7 @@ import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.ErrorTrait; import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait; +import software.amazon.smithy.typescript.codegen.documentation.StructureExampleGenerator; import software.amazon.smithy.typescript.codegen.endpointsV2.EndpointsParamNameMap; import software.amazon.smithy.typescript.codegen.endpointsV2.RuleSetParameterFinder; import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator; @@ -174,6 +175,9 @@ private String getCommandExample(String serviceName, String configName, String c + String.format("// const { %s, %s } = require(\"%s\"); // CommonJS import%n", serviceName, commandName, packageName) + String.format("const client = new %s(config);%n", serviceName) + + String.format("const input = %s%n", + StructureExampleGenerator.generateStructuralHintDocumentation( + model.getShape(operation.getInputShape()).get(), model)) + String.format("const command = new %s(input);%n", commandName) + "const response = await client.send(command);\n" + "```\n" diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/documentation/StructureExampleGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/documentation/StructureExampleGenerator.java new file mode 100644 index 00000000000..d6160b0a4da --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/documentation/StructureExampleGenerator.java @@ -0,0 +1,283 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.typescript.codegen.documentation; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.ListShape; +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.RequiredTrait; +import software.amazon.smithy.model.traits.StreamingTrait; + +/** + * Generates a structural hint for a shape used in command documentation. + */ +public abstract class StructureExampleGenerator { + /** + * Generates an example structure for API documentation, as an + * automated gap filler for operations that do not have + * hand written examples. + * + * Example for Athena::createPreparedStatement + * ```js + * const input = { + * // QueryStatement: 'STRING_VALUE', // required + * // StatementName: 'STRING_VALUE', // required + * // WorkGroup: 'STRING_VALUE', // required + * // Description: 'STRING_VALUE' + * }; + * ``` + */ + public static String generateStructuralHintDocumentation(Shape shape, Model model) { + StringBuilder buffer = new StringBuilder(); + shape(shape, buffer, model, 0, new ShapeTracker()); + + // replace non-leading whitespace with single space. + String s = Arrays.stream( + buffer.toString() + .split("\n")) + .map(line -> line.replaceAll( + "([\\w\\\",:\\[\\{] )\\s+", + "$1") + .replaceAll("\\s+$", "")) + .collect(Collectors.joining("\n")); + + return s.replaceAll(",$", ";"); + } + + private static void structure(StructureShape structureShape, + StringBuilder buffer, Model model, + int indentation, + ShapeTracker shapeTracker) { + if (structureShape.getAllMembers().size() == 0) { + append(indentation, buffer, "{},"); + checkRequired(indentation, buffer, structureShape); + } else { + append(indentation, buffer, "{"); + checkRequired(indentation, buffer, structureShape); + structureShape.getAllMembers().values().forEach(member -> { + append(indentation + 2, buffer, member.getMemberName() + ": "); + shape(member, buffer, model, indentation + 2, shapeTracker); + }); + append(indentation, buffer, "},\n"); + } + } + + private static void union(UnionShape unionShape, + StringBuilder buffer, + Model model, + int indentation, + ShapeTracker shapeTracker) { + append(indentation, buffer, "{ // Union: only one key present"); + checkRequired(indentation, buffer, unionShape); + unionShape.getAllMembers().values().forEach(member -> { + append(indentation + 2, buffer, member.getMemberName() + ": "); + shape(member, buffer, model, indentation + 2, shapeTracker); + }); + append(indentation, buffer, "},\n"); + } + + private static void shape(Shape shape, + StringBuilder buffer, + Model model, + int indentation, + ShapeTracker shapeTracker) { + Shape target; + if (shape instanceof MemberShape) { + target = model.getShape(((MemberShape) shape).getTarget()).get(); + } else { + target = shape; + } + + shapeTracker.mark(shape, indentation); + if (shapeTracker.getOccurrenceDepths(shape) > 2) { + append(indentation, buffer, "\"<" + shape.getId().getName() + ">\",\n"); + } else { + switch (target.getType()) { + case BIG_DECIMAL: + append(indentation, buffer, "Number(\"bigdecimal\"),"); + break; + case BIG_INTEGER: + append(indentation, buffer, "Number(\"bigint\"),"); + break; + case BLOB: + if (target.hasTrait(StreamingTrait.class)) { + append(indentation, buffer, "\"STREAMING_BLOB_VALUE\","); + } else { + append(indentation, buffer, "\"BLOB_VALUE\","); + } + break; + case BOOLEAN: + append(indentation, buffer, "true || false,"); + break; + case BYTE: + append(indentation, buffer, "\"BYTE_VALUE\","); + break; + case DOCUMENT: + append(indentation, buffer, "\"DOCUMENT_VALUE\","); + break; + case DOUBLE: + append(indentation, buffer, "Number(\"double\"),"); + break; + case FLOAT: + append(indentation, buffer, "Number(\"float\"),"); + break; + case INTEGER: + append(indentation, buffer, "Number(\"int\"),"); + break; + case LONG: + append(indentation, buffer, "Number(\"long\"),"); + break; + case SHORT: + append(indentation, buffer, "Number(\"short\"),"); + break; + case STRING: + append(indentation, buffer, "\"STRING_VALUE\","); + break; + case TIMESTAMP: + append(indentation, buffer, "new Date(\"TIMESTAMP\"),"); + break; + + case SET: + case LIST: + append(indentation, buffer, "["); + checkRequired(indentation, buffer, shape); + ListShape list = (ListShape) target; + shape(list.getMember(), buffer, model, indentation + 2, shapeTracker); + append(indentation, buffer, "],\n"); + break; + case MAP: + append(indentation, buffer, "{"); + checkRequired(indentation, buffer, shape); + append(indentation + 2, buffer, "\"\": "); + MapShape map = (MapShape) target; + shape(model.getShape(map.getValue().getTarget()).get(), buffer, model, indentation + 2, + shapeTracker); + append(indentation, buffer, "},\n"); + break; + + case STRUCTURE: + StructureShape structure = (StructureShape) target; + structure(structure, buffer, model, indentation, shapeTracker); + break; + case UNION: + UnionShape union = (UnionShape) target; + union(union, buffer, model, indentation, shapeTracker); + break; + + case ENUM: + EnumShape enumShape = (EnumShape) target; + String enumeration = enumShape.getEnumValues() + .values() + .stream() + .map(s -> "\"" + s + "\"") + .collect(Collectors.joining(" || ")); + append(indentation, buffer, enumeration + ","); + break; + case INT_ENUM: + IntEnumShape intEnumShape = (IntEnumShape) target; + String intEnumeration = intEnumShape.getEnumValues() + .values() + .stream() + .map(i -> Integer.toString(i)) + .collect(Collectors.joining(" || ")); + append(indentation, buffer, intEnumeration + ","); + break; + case OPERATION: + case RESOURCE: + case SERVICE: + case MEMBER: + default: + append(indentation, buffer, "\"...\","); + break; + } + + switch (target.getType()) { + case STRUCTURE: + case UNION: + case LIST: + case SET: + case MAP: + break; + case BIG_DECIMAL: + case BIG_INTEGER: + case BLOB: + case BOOLEAN: + case BYTE: + case DOCUMENT: + case DOUBLE: + case ENUM: + case FLOAT: + case INTEGER: + case INT_ENUM: + case LONG: + case MEMBER: + case OPERATION: + case RESOURCE: + case SERVICE: + case SHORT: + case STRING: + case TIMESTAMP: + default: + checkRequired(indentation, buffer, shape); + break; + } + } + } + + private static void checkRequired(int indentation, StringBuilder buffer, Shape shape) { + if (shape.hasTrait(RequiredTrait.class)) { + append(indentation, buffer, " // required\n"); + } else { + append(indentation, buffer, "\n"); + } + } + + private static void append(int indentation, StringBuilder buffer, String tail) { + while (indentation-- > 0) { + buffer.append(" "); + } + buffer.append(tail); + } + + /** + * Tracks the depths at which a shape appears in the tree. + * If a shape appears at too many depths it is truncated. + * This handles the case of recursive shapes. + */ + private static class ShapeTracker { + private Map> data = new HashMap>(); + + /** + * Mark that a shape is observed at depth. + */ + public void mark(Shape shape, int depth) { + if (!data.containsKey(shape)) { + data.put(shape, new HashSet<>()); + } + data.get(shape).add(depth); + } + + /** + * @return the number of distinct depths in which the shape appears. + */ + public int getOccurrenceDepths(Shape shape) { + return data.getOrDefault(shape, Collections.emptySet()).size(); + } + } +} diff --git a/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/documentation/StructureExampleGeneratorTest.java b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/documentation/StructureExampleGeneratorTest.java new file mode 100644 index 00000000000..9f769eb73a6 --- /dev/null +++ b/smithy-typescript-codegen/src/test/java/software/amazon/smithy/typescript/codegen/documentation/StructureExampleGeneratorTest.java @@ -0,0 +1,100 @@ +package software.amazon.smithy.typescript.codegen.documentation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import java.util.List; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; + +public class StructureExampleGeneratorTest { + + StringShape string = StringShape.builder() + .id("foo.bar#string") + .build(); + + ListShape list = ListShape.builder() + .id("foo.bar#list") + .member(string.getId()) + .build(); + + MapShape map = MapShape.builder() + .id("foo.bar#map") + .key(MemberShape.builder() + .id("foo.bar#map$member") + .target(string.getId()) + .build()) + .value(MemberShape.builder() + .id("foo.bar#map$member") + .target(string.getId()) + .build()) + .build(); + + MemberShape memberForString = MemberShape.builder() + .id("foo.bar#structure$string") + .target(string.getId()) + .build(); + + MemberShape memberForList = MemberShape.builder() + .id("foo.bar#structure$list") + .target(list.getId()) + .build(); + + MemberShape memberForMap = MemberShape.builder() + .id("foo.bar#structure$map") + .target(map.getId()) + .build(); + + StructureShape structure = StructureShape.builder() + .id("foo.bar#structure") + .members( + List.of(memberForString, memberForList, memberForMap)) + .build(); + + private Model model = Model.builder() + .addShapes( + string, list, map, structure, + memberForString, memberForList, memberForMap) + .build(); + + @Test + public void generatesStructuralHintDocumentation_map() { + assertThat( + StructureExampleGenerator.generateStructuralHintDocumentation(map, model), + equalTo(""" + { + "": "STRING_VALUE", + };""")); + } + + @Test + public void generatesStructuralHintDocumentation_structure() { + assertThat( + StructureExampleGenerator.generateStructuralHintDocumentation(structure, model), + equalTo(""" + { + string: "STRING_VALUE", + list: [ + "STRING_VALUE", + ], + map: { + "": "STRING_VALUE", + }, + };""")); + } + + @Test + public void generatesStructuralHintDocumentation_list() { + assertThat( + StructureExampleGenerator.generateStructuralHintDocumentation(list, model), + equalTo(""" + [ + "STRING_VALUE", + ];""")); + } +}