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 0f4011f1966..cdae6c9e497 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 @@ -38,6 +38,7 @@ import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.OperationIndex; import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; @@ -47,8 +48,10 @@ import software.amazon.smithy.model.traits.DeprecatedTrait; import software.amazon.smithy.model.traits.DocumentationTrait; import software.amazon.smithy.model.traits.ErrorTrait; +import software.amazon.smithy.model.traits.ExamplesTrait; import software.amazon.smithy.model.traits.InternalTrait; import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait; +import software.amazon.smithy.typescript.codegen.documentation.DocumentationExampleGenerator; import software.amazon.smithy.typescript.codegen.documentation.StructureExampleGenerator; import software.amazon.smithy.typescript.codegen.endpointsV2.RuleSetParameterFinder; import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator; @@ -130,11 +133,13 @@ private void generateClientCommand() { String name = symbol.getName(); StringBuilder additionalDocs = new StringBuilder() - .append("\n") - .append(getCommandExample( - serviceSymbol.getName(), configType, name, inputType.getName(), outputType.getName())) - .append("\n") - .append(getThrownExceptions()); + .append("\n") + .append(getCommandExample( + serviceSymbol.getName(), configType, name, inputType.getName(), outputType.getName())) + .append("\n") + .append(getThrownExceptions()) + .append("\n") + .append(getCuratedExamples(name)); boolean operationHasDocumentation = operation.hasTrait(DocumentationTrait.class); @@ -199,10 +204,12 @@ private void generateClientCommand() { writer.write("}"); // class close bracket. } - private String getCommandExample(String serviceName, String configName, String commandName, String commandInput, - String commandOutput) { + private String getCommandExample( + String serviceName, String configName, String commandName, + String commandInput, String commandOutput + ) { String packageName = settings.getPackageName(); - return "@example\n" + String exampleDoc = "@example\n" + "Use a bare-bones client and the command you need to make an API call.\n" + "```javascript\n" + String.format("import { %s, %s } from \"%s\"; // ES Modules import%n", serviceName, commandName, @@ -225,6 +232,46 @@ private String getCommandExample(String serviceName, String configName, String c + String.format("@see {@link %s} for command's `input` shape.%n", commandInput) + String.format("@see {@link %s} for command's `response` shape.%n", commandOutput) + String.format("@see {@link %s | config} for %s's `config` shape.%n", configName, serviceName); + + return exampleDoc; + } + + /** + * Handwritten examples from the operation ExamplesTrait. + */ + private String getCuratedExamples(String commandName) { + String exampleDoc = ""; + if (operation.getTrait(ExamplesTrait.class).isPresent()) { + List examples = operation.getTrait(ExamplesTrait.class).get().getExamples(); + StringBuilder buffer = new StringBuilder(); + + for (ExamplesTrait.Example example : examples) { + ObjectNode input = example.getInput(); + Optional output = example.getOutput(); + buffer + .append("\n") + .append(String.format("@example %s%n", example.getTitle())) + .append("```javascript\n") + .append(String.format("/* %s */%n", example.getDocumentation().orElse(""))) + .append(""" + const input = %s; + const command = new %s(input); + const response = await client.send(command); + /* response is + %s + */ + """.formatted( + DocumentationExampleGenerator.inputToJavaScriptObject(input), + commandName, + DocumentationExampleGenerator.outputToJavaScriptObject(output.orElse(null)) + )) + .append("```") + .append("\n"); + } + + exampleDoc += buffer.toString(); + } + return exampleDoc; } private String getThrownExceptions() { diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/documentation/DocumentationExampleGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/documentation/DocumentationExampleGenerator.java new file mode 100644 index 00000000000..3476ea15efa --- /dev/null +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/documentation/DocumentationExampleGenerator.java @@ -0,0 +1,102 @@ +/* + * 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.stream.Collectors; +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.BooleanNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NullNode; +import software.amazon.smithy.model.node.NumberNode; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.utils.SmithyInternalApi; + +@SmithyInternalApi +public final class DocumentationExampleGenerator { + private DocumentationExampleGenerator() {} + + /** + * @return the ObjectNode from the curated example written as a JavaScript object literal. + */ + public static String inputToJavaScriptObject(ObjectNode node) { + if (node == null) { + return "{ /* empty */ }"; + } + return write(node, 0); + } + + public static String outputToJavaScriptObject(ObjectNode node) { + if (node == null) { + return "{ /* metadata only */ }"; + } + return write(node, 0); + } + + private static String write(Node node, int indent) { + StringBuilder buffer = new StringBuilder(); + String indentation = " ".repeat(indent); + + switch (node.getType()) { + case OBJECT -> { + ObjectNode objectNode = node.expectObjectNode(); + if (objectNode.getMembers().isEmpty()) { + return indentation + "{ /* empty */ }"; + } + String membersJoined = objectNode.getMembers() + .entrySet() + .stream() + .map(entry -> indentation + + " " + + entry.getKey().getValue() + + ": " + + write(entry.getValue(), indent + 2)) + .collect(Collectors.joining(",\n")); + + return buffer + .append("{\n") + .append(membersJoined).append("\n") + .append(indentation).append("}") + .toString(); + } + case ARRAY -> { + ArrayNode arrayNode = node.expectArrayNode(); + if (arrayNode.getElements().isEmpty()) { + return indentation + "[]"; + } + String membersJoined = arrayNode.getElements() + .stream() + .map(elementNode -> indentation + + " " + + write(elementNode, indent + 2)) + .collect(Collectors.joining(",\n")); + + return buffer + .append("[\n") + .append(membersJoined).append("\n") + .append(indentation).append("]") + .toString(); + } + case STRING -> { + StringNode stringNode = node.expectStringNode(); + return "\"" + stringNode.getValue() + "\""; + } + case NUMBER -> { + NumberNode numberNode = node.expectNumberNode(); + return numberNode.getValue().toString(); + } + case BOOLEAN -> { + BooleanNode booleanNode = node.expectBooleanNode(); + return booleanNode.toString(); + } + case NULL -> { + NullNode nullNode = node.expectNullNode(); + return nullNode.toString(); + } + default -> throw new IllegalStateException("Unexpected value: " + node.getType()); + } + } +}