From aadca849b5c187467911f9fb4f16f33d280b5c58 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 12 Dec 2025 18:26:19 +0100 Subject: [PATCH 1/2] Add generator for shape initializers This adds a generator that writes out shape initializers given their value as a node. This is primarily intended for examples, but could be used for other purposes. --- .../codegen/generators/SnippetGenerator.java | 647 ++++++++++++++++++ .../java/codegen/writer/JavaWriter.java | 9 + .../generators/SnippetGeneratorTest.java | 300 ++++++++ .../codegen/generators/snippet-test.smithy | 45 ++ 4 files changed, 1001 insertions(+) create mode 100644 codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SnippetGenerator.java create mode 100644 codegen/codegen-core/src/test/java/software/amazon/smithy/java/codegen/generators/SnippetGeneratorTest.java create mode 100644 codegen/codegen-core/src/test/resources/software/amazon/smithy/java/codegen/generators/snippet-test.smithy diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SnippetGenerator.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SnippetGenerator.java new file mode 100644 index 000000000..ae5d0cc31 --- /dev/null +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/generators/SnippetGenerator.java @@ -0,0 +1,647 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.generators; + +import static software.amazon.smithy.java.core.serde.TimestampFormatter.Prelude.DATE_TIME; +import static software.amazon.smithy.java.core.serde.TimestampFormatter.Prelude.EPOCH_SECONDS; +import static software.amazon.smithy.java.core.serde.TimestampFormatter.Prelude.HTTP_DATE; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.java.codegen.CodeGenerationContext; +import software.amazon.smithy.java.codegen.SymbolProperties; +import software.amazon.smithy.java.codegen.writer.JavaWriter; +import software.amazon.smithy.java.core.serde.document.Document; +import software.amazon.smithy.java.io.datastream.DataStream; +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.NodeVisitor; +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.model.shapes.BigDecimalShape; +import software.amazon.smithy.model.shapes.BigIntegerShape; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.ByteShape; +import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.LongShape; +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.ShapeVisitor; +import software.amazon.smithy.model.shapes.ShortShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.TimestampShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Generates Java snippets. + */ +@SmithyInternalApi +public final class SnippetGenerator { + + private SnippetGenerator() {} + + /** + * Generates Java code to initialize a shape based on a Node value. + * + *

This does not use the deserialize method, but rather directly initializes the + * shape by converting the node value to Java code. This enables support for + * non-structured shapes and enables more natural examples. + * + *

Imports, headers, and package declarations are omitted. To include those, use + * {@link #writeShapeInitializer} instead. + * + * @param context The code generation context. + * @param shape The shape to initialize. + * @param value The value to initialize the shape with. + * @return Returns a string containing Java code initializing the given shape with + * the given value. + */ + public static String generateShapeInitializer( + CodeGenerationContext context, + Shape shape, + Node value + ) { + var writer = new JavaWriter(context.settings(), context.settings().packageNamespace(), "Snippet.java"); + writeShapeInitializer(context, shape, value, writer); + return writer.toContentString().strip(); + } + + /** + * Writes Java code to initialize a shape based on a Node value. + * + *

This does not use the deserialize method, but rather directly initializes the + * shape by converting the node value to Java code. This enables support for + * non-structured shapes and enables more natural examples. + * + *

Imports, headers, and package declarations are included. To exclude those, use + * {@link #generateShapeInitializer} instead. + * + * @param context The code generation context. + * @param shape The shape to initialize. + * @param value The value to initialize the shape with. + * @param writer The writer to write to. + */ + public static void writeShapeInitializer( + CodeGenerationContext context, + Shape shape, + Node value, + JavaWriter writer + ) { + var visitor = new ShapeSnippetVisitor(context, writer, value); + shape.accept(visitor); + } + + /** + * Visitor that generates initializers for shapes based on a node value. + */ + private static final class ShapeSnippetVisitor extends ShapeVisitor.DataShapeVisitor { + private final CodeGenerationContext context; + private final JavaWriter writer; + private final Node value; + + ShapeSnippetVisitor(CodeGenerationContext context, JavaWriter writer, Node value) { + this.context = context; + this.writer = writer; + this.value = value; + } + + @Override + public Void blobShape(BlobShape shape) { + // We don't have the context to know if a shape should be nullable, but even + // if we did there's cases where we would want to write out null anyway, such + // as in examples that deliberately showcase that validation. + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + var stringValue = value.expectStringNode().getValue(); + + // Blob values in smithy nodes are *supposed* to be base64 encoded strings, + // but in practice they are sometimes just UTF8 strings. So here we check + // to see which is the case and handle each. + if (isBase64(stringValue)) { + writer.writeInline( + "$T.ofBytes($T.getDecoder().decode($S))", + DataStream.class, + Base64.class, + stringValue); + } else { + writer.writeInline("$T.ofString($S)", DataStream.class, stringValue); + } + return null; + } + + private boolean isBase64(String value) { + // Java is lenient on padding, but we don't want to be. + if (value.length() % 4 != 0) { + return false; + } + try { + Base64.getDecoder().decode(value.getBytes(StandardCharsets.UTF_8)); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + @Override + public Void booleanShape(BooleanShape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + writer.writeInline("$L", value.expectBooleanNode().getValue() ? "true" : "false"); + return null; + } + + @Override + public Void byteShape(ByteShape shape) { + numericShape(); + return null; + } + + @Override + public Void shortShape(ShortShape shape) { + numericShape(); + return null; + } + + @Override + public Void integerShape(IntegerShape shape) { + numericShape(); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + writeEnumShape(shape); + return null; + } + + private void writeEnumShape(Shape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return; + } + var symbol = context.symbolProvider().toSymbol(shape); + var member = getMember(shape, value); + if (member.isPresent()) { + var memberName = context.symbolProvider().toMemberName(member.get()); + writer.writeInline("$T.$L", symbol, memberName); + } else { + if (shape.isEnumShape()) { + writer.writeInline("$T.unknown($S)", symbol, value.expectStringNode().getValue()); + } else { + writer.writeInline("$T.unknown($L)", symbol, value.expectNumberNode().getValue()); + } + } + + } + + private Optional getMember(Shape shape, Node value) { + for (MemberShape member : shape.members()) { + var trait = member.expectTrait(EnumValueTrait.class); + if (trait.toNode().equals(value)) { + return Optional.of(member); + } + } + return Optional.empty(); + } + + @Override + public Void longShape(LongShape shape) { + numericShape(); + return null; + } + + @Override + public Void floatShape(FloatShape shape) { + numericShape(); + return null; + } + + @Override + public Void doubleShape(DoubleShape shape) { + numericShape(); + return null; + } + + private void numericShape() { + if (value.isNullNode()) { + writer.writeInline("null"); + return; + } + writer.writeInline("$L", value.expectNumberNode().getValue()); + } + + @Override + public Void bigIntegerShape(BigIntegerShape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + var innerValue = value.asNumberNode() + .map(n -> n.getValue().toString()) + .orElseGet(() -> value.expectStringNode().getValue()); + writer.writeInline("new $T($S)", BigInteger.class, innerValue); + return null; + } + + @Override + public Void bigDecimalShape(BigDecimalShape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + var innerValue = value.asNumberNode() + .map(n -> n.getValue().toString()) + .orElseGet(() -> value.expectStringNode().getValue()); + writer.writeInline("new $T($S)", BigDecimal.class, innerValue); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + writer.writeInline("$S", value.expectStringNode().getValue()); + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + writeEnumShape(shape); + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + var instant = parseTimestampNode(value); + writer.writeInline("$T.parse($S)", Instant.class, instant); + return null; + } + + // Note that we're not attempting to use the timestamp format trait to figure + // out what the format is because the serialized format isn't relevant to the + // format presented in the value node. + private Instant parseTimestampNode(Node node) { + if (node.isNumberNode()) { + return EPOCH_SECONDS.readFromNumber(node.expectNumberNode().getValue()); + } + + var stringValue = node.expectStringNode().getValue(); + try { + return DATE_TIME.readFromString(stringValue, false); + } catch (DateTimeParseException e) { + return HTTP_DATE.readFromString(stringValue, false); + } + } + + @Override + public Void listShape(ListShape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + + var symbol = context.symbolProvider().toSymbol(shape); + var listValue = value.expectArrayNode(); + if (listValue.isEmpty()) { + var method = symbol.expectProperty(SymbolProperties.COLLECTION_EMPTY_METHOD); + writer.writeInline("$T.$L", Collections.class, method); + return null; + } + + // Lists have the COLLECTION_IMPLEMENTATION_CLASS symbol property that defines + // what class should be used by default in the deserialize methods. That isn't + // checked here because these are primarily used for examples, and the extra + // complexity would be a hindrance. + if (listValue.size() == 1) { + // If there's only one entry, we don't need to put in newlines + writer.write("$T.of(${C|})", List.class, writer.consumer(w -> listMembers(shape, listValue))); + return null; + } + + writer.write( + """ + $T.of( + ${C|} + )""", + List.class, + writer.consumer(w -> listMembers(shape, listValue))); + return null; + } + + private void listMembers(ListShape shape, ArrayNode listValue) { + var member = shape.getMember(); + var iterator = listValue.getElements().iterator(); + while (iterator.hasNext()) { + var element = iterator.next(); + member.accept(new ShapeSnippetVisitor(context, writer, element)); + if (iterator.hasNext()) { + writer.write(","); + } + } + } + + @Override + public Void mapShape(MapShape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + + var symbol = context.symbolProvider().toSymbol(shape); + var mapValue = value.expectObjectNode(); + if (mapValue.isEmpty()) { + var method = symbol.expectProperty(SymbolProperties.COLLECTION_EMPTY_METHOD); + writer.writeInline("$T.$L", Collections.class, method); + return null; + } + + writer.pushState(); + + // The more compact form of Map.of is limited to 10 entries, this context + // variable is used to prefer the compact form when possible. + writer.putContext("compact", mapValue.size() <= 10); + var memberConsumer = writer.consumer(w -> mapMembers(shape, mapValue)); + + // Maps have the COLLECTION_IMPLEMENTATION_CLASS symbol property that defines + // what class should be used by default in the deserialize methods. That isn't + // checked here for two reasons. First, these are primarily used for examples, + // where the extra complexity would be a hindrance. But also, it may not be + // possible to initialize these alternate implementations in a way that lets + // them be useful. Notably, LinkedHashMap, the default implementation class, + // would be difficult to initialize inline. Wrapping Map.of would defeat the + // point, and using streams or some other workaround would be cumbersome. + if (mapValue.size() == 1) { + // If there's only one entry, we don't need to put in newlines + writer.writeInline("$T.of(${C|})", Map.class, memberConsumer); + } else { + writer.writeInline(""" + $T.of${^compact}Entries${/compact}( + ${C|} + )""", + Map.class, + memberConsumer); + } + + writer.popState(); + return null; + } + + private void mapMembers(MapShape shape, ObjectNode mapValue) { + var keyShape = shape.getKey(); + var valueShape = shape.getValue(); + var iterator = mapValue.getMembers().entrySet().iterator(); + + while (iterator.hasNext()) { + var entry = iterator.next(); + + // The key can't just be written out as a string, because *technically* it could be an enum. + var keyVisitor = new ShapeSnippetVisitor(context, writer, entry.getKey()); + var keyConsumer = writer.consumer(w -> keyShape.accept(keyVisitor)); + + var valueVisitor = new ShapeSnippetVisitor(context, writer, entry.getValue()); + var valueConsumer = writer.consumer(w -> valueShape.accept(valueVisitor)); + + writer.writeInline(""" + ${^compact}Map.entry(${/compact}\ + ${C|}, ${C|}\ + ${^compact})${/compact}""", + keyConsumer, + valueConsumer); + + if (iterator.hasNext()) { + writer.write(","); + } + } + } + + @Override + public Void structureShape(StructureShape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + var symbol = context.symbolProvider().toSymbol(shape); + writer.writeInline(""" + $T.builder() + ${C|} + .build() + """, symbol, writer.consumer(w -> structureMembers(shape))); + return null; + } + + private void structureMembers(Shape shape) { + ObjectNode structValue = value.expectObjectNode(); + for (Map.Entry entry : structValue.getMembers().entrySet()) { + var member = shape.getMember(entry.getKey().getValue()) + .orElseThrow(() -> new CodegenException(String.format( + "Tried to bind key \"%s\" to a member of %s, but no matching member name was found.", + entry.getKey().getValue(), + shape.getId()))); + var memberName = context.symbolProvider().toMemberName(member); + var memberVisitor = new ShapeSnippetVisitor(context, writer, entry.getValue()); + writer.writeInline(".$L(${C|})", + memberName, + writer.consumer(w -> member.accept(memberVisitor))); + } + } + + @Override + public Void unionShape(UnionShape shape) { + if (value.isNullNode()) { + writer.writeInline("null"); + return null; + } + var symbol = context.symbolProvider().toSymbol(shape); + + var members = value.expectObjectNode().getMembers(); + if (members.size() != 1) { + throw new CodegenException("Union value must have exactly one member, but found " + members.size()); + } + + var member = members.entrySet().iterator().next(); + var memberName = member.getKey().getValue(); + var memberShape = shape.getMember(memberName); + + if (memberShape.isEmpty()) { + writer.writeInline("$T.$$Unknown($S)", symbol, memberName); + return null; + } + memberName = context.symbolProvider().toMemberName(memberShape.get()); + + var memberVisitor = new ShapeSnippetVisitor(context, writer, member.getValue()); + writer.writeInline("$T.$UMember(${C|})", + symbol, + memberName, + writer.consumer(w -> memberShape.get().accept(memberVisitor))); + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + value.accept(new DocumentSnippetVisitor(writer)); + return null; + } + + @Override + public Void memberShape(MemberShape shape) { + return context.model().expectShape(shape.getTarget()).accept(this); + } + } + + /** + * Visitor that generates an initializer for a Document based on a node value. + */ + private static final class DocumentSnippetVisitor implements NodeVisitor { + + private final JavaWriter writer; + + DocumentSnippetVisitor(JavaWriter writer) { + this.writer = writer; + } + + @Override + public Void booleanNode(BooleanNode node) { + writer.writeInline("$T.of($L)", Document.class, node.expectBooleanNode().getValue()); + return null; + } + + @Override + public Void nullNode(NullNode node) { + writer.writeInline("null"); + return null; + } + + @Override + public Void numberNode(NumberNode node) { + writer.writeInline("$T.of($L)", Document.class, node.expectNumberNode().getValue()); + return null; + } + + @Override + public Void stringNode(StringNode node) { + writer.writeInline("$T.of($S)", Document.class, node.expectStringNode().getValue()); + return null; + } + + @Override + public Void arrayNode(ArrayNode node) { + if (node.isEmpty()) { + writer.writeInline("$T.of($T.emptyList())", Document.class, Collections.class); + return null; + } + if (node.size() == 1) { + writer.writeInline("$T.of($T.of(${C|}))", + Document.class, + List.class, + writer.consumer(w -> arrayMembers(node))); + } else { + writer.writeInline( + """ + $T.of($T.of( + ${C|} + ))""", + Document.class, + List.class, + writer.consumer(w -> arrayMembers(node))); + } + + return null; + } + + private void arrayMembers(ArrayNode listValue) { + var iterator = listValue.getElements().iterator(); + while (iterator.hasNext()) { + var element = iterator.next(); + element.accept(this); + if (iterator.hasNext()) { + writer.write(","); + } + } + } + + @Override + public Void objectNode(ObjectNode node) { + if (node.isEmpty()) { + writer.writeInline("$T.of($T.emptyMap())", Document.class, Collections.class); + return null; + } + + writer.pushState(); + + // The more compact form of Map.of is limited to 10 entries, this context + // variable is used to prefer the compact form when possible. + writer.putContext("compact", node.size() <= 10); + var memberConsumer = writer.consumer(w -> objectMembers(node)); + + if (node.size() == 1) { + writer.writeInline("$T.of($T.of(${C|}))", Document.class, Map.class, memberConsumer); + } else { + writer.writeInline(""" + $T.of($T.of${^compact}Entries${/compact}( + ${C|} + ))""", + Document.class, + Map.class, + memberConsumer); + } + + writer.popState(); + return null; + } + + private void objectMembers(ObjectNode node) { + var iterator = node.getMembers().entrySet().iterator(); + + while (iterator.hasNext()) { + var entry = iterator.next(); + + writer.writeInline(""" + ${^compact}Map.entry(${/compact}\ + $S, ${C|}\ + ${^compact})${/compact}""", + entry.getKey().getValue(), + writer.consumer(w -> entry.getValue().accept(this))); + + if (iterator.hasNext()) { + writer.write(","); + } + } + } + } +} diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/writer/JavaWriter.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/writer/JavaWriter.java index 7fc083f8e..ebd14c127 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/writer/JavaWriter.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/writer/JavaWriter.java @@ -82,6 +82,15 @@ public String toString() { super.toString()); } + /** + * @return Returns the formatted writer contents without headers, package declarations, or imports. + */ + public String toContentString() { + putNameContext(); + setExpressionStart(PLACEHOLDER_FORMAT_CHAR); + return format("£L", super.toString()); + } + public void newLine() { writeInlineWithNoFormatting(getNewline()); } diff --git a/codegen/codegen-core/src/test/java/software/amazon/smithy/java/codegen/generators/SnippetGeneratorTest.java b/codegen/codegen-core/src/test/java/software/amazon/smithy/java/codegen/generators/SnippetGeneratorTest.java new file mode 100644 index 000000000..6beeac506 --- /dev/null +++ b/codegen/codegen-core/src/test/java/software/amazon/smithy/java/codegen/generators/SnippetGeneratorTest.java @@ -0,0 +1,300 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.generators; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.Objects; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.java.codegen.CodeGenerationContext; +import software.amazon.smithy.java.codegen.utils.TestJavaCodegenPlugin; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; + +public class SnippetGeneratorTest { + + private static CodeGenerationContext context; + + @BeforeAll + public static void setUp() { + var model = Model.assembler() + .addImport(Objects.requireNonNull(SnippetGeneratorTest.class.getResource("snippet-test.smithy"))) + .assemble() + .unwrap(); + + var plugin = new TestJavaCodegenPlugin(); + var pluginContext = PluginContext.builder() + .fileManifest(new MockManifest()) + .settings( + ObjectNode.builder() + .withMember("service", "smithy.java.codegen#TestService") + .withMember("namespace", "software.amazon.smithy.java.codegen.test") + .build()) + .model(model) + .build(); + plugin.execute(pluginContext); + + context = plugin.capturedContext; + } + + public static Stream cases() { + return Stream.of( + arguments("smithy.api#Blob", Node.from("foo"), "DataStream.ofString(\"foo\")"), + arguments("smithy.api#Blob", + Node.from("Zm9v"), + "DataStream.ofBytes(Base64.getDecoder().decode(\"Zm9v\"))"), + arguments("smithy.api#Blob", Node.nullNode(), "null"), + + arguments("smithy.api#Boolean", Node.from(true), "true"), + arguments("smithy.api#Boolean", Node.from(false), "false"), + arguments("smithy.api#Boolean", Node.nullNode(), "null"), + + arguments("smithy.api#Byte", Node.from(1), "1"), + arguments("smithy.api#Byte", Node.nullNode(), "null"), + + arguments("smithy.api#Short", Node.from(1), "1"), + arguments("smithy.api#Short", Node.nullNode(), "null"), + + arguments("smithy.api#Integer", Node.from(1), "1"), + arguments("smithy.api#Integer", Node.nullNode(), "null"), + + arguments("smithy.java.codegen#IntEnum", Node.from(1), "IntEnum.BAR"), + arguments("smithy.java.codegen#IntEnum", Node.from(8), "IntEnum.unknown(8)"), + arguments("smithy.java.codegen#IntEnum", Node.nullNode(), "null"), + + arguments("smithy.api#Long", Node.from(1), "1"), + arguments("smithy.api#Long", Node.nullNode(), "null"), + + arguments("smithy.api#BigInteger", Node.from(1), "new BigInteger(\"1\")"), + arguments("smithy.api#BigInteger", Node.from("1"), "new BigInteger(\"1\")"), + arguments("smithy.api#BigInteger", Node.nullNode(), "null"), + + arguments("smithy.api#Float", Node.from(1.1), "1.1"), + arguments("smithy.api#Float", Node.nullNode(), "null"), + + arguments("smithy.api#Double", Node.from(1.1), "1.1"), + arguments("smithy.api#Double", Node.nullNode(), "null"), + + arguments("smithy.api#BigDecimal", Node.from(1.1), "new BigDecimal(\"1.1\")"), + arguments("smithy.api#BigDecimal", Node.from("1.1"), "new BigDecimal(\"1.1\")"), + arguments("smithy.api#BigDecimal", Node.nullNode(), "null"), + + arguments("smithy.api#String", Node.from("foo"), "\"foo\""), + arguments("smithy.api#String", Node.nullNode(), "null"), + + arguments("smithy.java.codegen#StringEnum", Node.from("FOO"), "StringEnum.FOO"), + arguments("smithy.java.codegen#StringEnum", Node.from("unknown"), "StringEnum.unknown(\"unknown\")"), + arguments("smithy.java.codegen#StringEnum", Node.nullNode(), "null"), + + arguments("smithy.api#Timestamp", Node.from(1553372880), "Instant.parse(\"2019-03-23T20:28:00Z\")"), + arguments("smithy.api#Timestamp", + Node.from(1553372880.1), + "Instant.parse(\"2019-03-23T20:28:00.100Z\")"), + arguments("smithy.api#Timestamp", + Node.from("2019-03-23T20:28:00Z"), + "Instant.parse(\"2019-03-23T20:28:00Z\")"), + arguments("smithy.api#Timestamp", + Node.from("2019-03-23T20:28:00.100Z"), + "Instant.parse(\"2019-03-23T20:28:00.100Z\")"), + arguments("smithy.api#Timestamp", + Node.from("Sat, 23 Mar 2019 20:28:00 GMT"), + "Instant.parse(\"2019-03-23T20:28:00Z\")"), + arguments("smithy.api#Timestamp", Node.nullNode(), "null"), + + arguments("smithy.java.codegen#StringList", Node.arrayNode(), "Collections.emptyList()"), + arguments("smithy.java.codegen#StringList", Node.fromStrings("foo"), "List.of(\"foo\")"), + arguments("smithy.java.codegen#StringList", + Node.fromStrings("foo", "bar", "baz"), + """ + List.of( + "foo", + "bar", + "baz" + )"""), + arguments("smithy.java.codegen#StringList", Node.nullNode(), "null"), + + arguments("smithy.java.codegen#StringMap", ObjectNode.builder().build(), "Collections.emptyMap()"), + arguments( + "smithy.java.codegen#StringMap", + ObjectNode.builder() + .withMember("foo", "bar") + .build(), + "Map.of(\"foo\", \"bar\")"), + arguments( + "smithy.java.codegen#StringMap", + ObjectNode.builder() + .withMember("foo", "bar") + .withMember("spam", "eggs") + .build(), + """ + Map.of( + "foo", "bar", + "spam", "eggs" + )"""), + arguments( + "smithy.java.codegen#StringMap", + ObjectNode.builder() + .withMember("one", "one") + .withMember("two", "two") + .withMember("three", "three") + .withMember("four", "four") + .withMember("five", "five") + .withMember("six", "six") + .withMember("seven", "seven") + .withMember("eight", "eight") + .withMember("nine", "nine") + .withMember("ten", "ten") + .withMember("eleven", "eleven") + .build(), + """ + Map.ofEntries( + Map.entry("one", "one"), + Map.entry("two", "two"), + Map.entry("three", "three"), + Map.entry("four", "four"), + Map.entry("five", "five"), + Map.entry("six", "six"), + Map.entry("seven", "seven"), + Map.entry("eight", "eight"), + Map.entry("nine", "nine"), + Map.entry("ten", "ten"), + Map.entry("eleven", "eleven") + )"""), + arguments( + "smithy.java.codegen#EnumKeyMap", + ObjectNode.builder() + .withMember("FOO", "bar") + .build(), + "Map.of(StringEnum.FOO, \"bar\")"), + arguments("smithy.java.codegen#StringMap", Node.nullNode(), "null"), + + arguments( + "smithy.java.codegen#TestOperationInput", + ObjectNode.builder() + .withMember("foo", "bar") + .build(), + """ + TestOperationInput.builder() + .foo("bar") + .build()"""), + arguments("smithy.java.codegen#TestOperationInput", Node.nullNode(), "null"), + + arguments( + "smithy.java.codegen#BasicUnion", + ObjectNode.builder() + .withMember("foo", "bar") + .build(), + "BasicUnion.FooMember(\"bar\")"), + arguments( + "smithy.java.codegen#BasicUnion", + ObjectNode.builder() + .withMember("spam", "eggs") + .build(), + "BasicUnion.$Unknown(\"spam\")"), + arguments("smithy.java.codegen#BasicUnion", Node.nullNode(), "null"), + + arguments("smithy.api#Document", Node.from("foo"), "Document.of(\"foo\")"), + arguments("smithy.api#Document", Node.from(1), "Document.of(1)"), + arguments("smithy.api#Document", Node.from(1.1), "Document.of(1.1)"), + arguments("smithy.api#Document", Node.from(true), "Document.of(true)"), + arguments("smithy.api#Document", Node.from(false), "Document.of(false)"), + arguments("smithy.api#Document", Node.arrayNode(), "Document.of(Collections.emptyList())"), + arguments("smithy.api#Document", Node.fromStrings("foo"), "Document.of(List.of(Document.of(\"foo\")))"), + arguments("smithy.api#Document", + Node.fromStrings("foo", "bar", "baz"), + """ + Document.of(List.of( + Document.of("foo"), + Document.of("bar"), + Document.of("baz") + ))"""), + arguments("smithy.api#Document", + Node.arrayNode(Node.from("foo"), Node.from(1), Node.from("bar")), + """ + Document.of(List.of( + Document.of("foo"), + Document.of(1), + Document.of("bar") + ))"""), + arguments("smithy.api#Document", ObjectNode.builder().build(), "Document.of(Collections.emptyMap())"), + arguments( + "smithy.api#Document", + ObjectNode.builder() + .withMember("foo", "bar") + .build(), + "Document.of(Map.of(\"foo\", Document.of(\"bar\")))"), + arguments( + "smithy.api#Document", + ObjectNode.builder() + .withMember("foo", "bar") + .withMember("spam", "eggs") + .build(), + """ + Document.of(Map.of( + "foo", Document.of("bar"), + "spam", Document.of("eggs") + ))"""), + arguments( + "smithy.api#Document", + ObjectNode.builder() + .withMember("one", "one") + .withMember("two", "two") + .withMember("three", "three") + .withMember("four", "four") + .withMember("five", "five") + .withMember("six", "six") + .withMember("seven", "seven") + .withMember("eight", "eight") + .withMember("nine", "nine") + .withMember("ten", "ten") + .withMember("eleven", "eleven") + .build(), + """ + Document.of(Map.ofEntries( + Map.entry("one", Document.of("one")), + Map.entry("two", Document.of("two")), + Map.entry("three", Document.of("three")), + Map.entry("four", Document.of("four")), + Map.entry("five", Document.of("five")), + Map.entry("six", Document.of("six")), + Map.entry("seven", Document.of("seven")), + Map.entry("eight", Document.of("eight")), + Map.entry("nine", Document.of("nine")), + Map.entry("ten", Document.of("ten")), + Map.entry("eleven", Document.of("eleven")) + ))"""), + arguments( + "smithy.api#Document", + ObjectNode.builder() + .withMember("foo", "bar") + .withMember("spam", 1) + .build(), + """ + Document.of(Map.of( + "foo", Document.of("bar"), + "spam", Document.of(1) + ))"""), + arguments("smithy.api#Document", Node.nullNode(), "null")); + } + + @ParameterizedTest + @MethodSource("cases") + public void testGenerateShapeInitializer(String shapeId, Node value, String expected) { + var shape = context.model().expectShape(ShapeId.from(shapeId)); + var actual = SnippetGenerator.generateShapeInitializer(context, shape, value); + assertThat(actual, equalTo(expected)); + } +} diff --git a/codegen/codegen-core/src/test/resources/software/amazon/smithy/java/codegen/generators/snippet-test.smithy b/codegen/codegen-core/src/test/resources/software/amazon/smithy/java/codegen/generators/snippet-test.smithy new file mode 100644 index 000000000..be6f7815c --- /dev/null +++ b/codegen/codegen-core/src/test/resources/software/amazon/smithy/java/codegen/generators/snippet-test.smithy @@ -0,0 +1,45 @@ +$version: "2" + +namespace smithy.java.codegen + +service TestService { + operations: [ + TestOperation + ] +} + +operation TestOperation { + input := { + foo: String + stringList: StringList + stringEnum: StringEnum + intEnum: IntEnum + stringMap: StringMap + } +} + +list StringList { + member: String +} + +enum StringEnum { + FOO +} + +intEnum IntEnum { + BAR = 1 +} + +map StringMap { + key: String + value: String +} + +map EnumKeyMap { + key: StringEnum + value: String +} + +union BasicUnion { + foo: String +} From affaaaa59ef6951c4412bbf7044b9036c341881e Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Mon, 15 Dec 2025 18:46:05 +0100 Subject: [PATCH 2/2] Generate examples This adds support for the examples trait to javadoc generation for clients. --- .../java/codegen/writer/JavaWriter.java | 14 ++ .../ClientJavadocExamplesIntegration.java | 44 ++++++ .../javadoc/ExamplesTraitInterceptor.java | 139 ++++++++++++++++++ ...smithy.java.codegen.JavaCodegenIntegration | 1 + .../ClientJavadocExamplesIntegrationTest.java | 63 ++++++++ .../client/utils/AbstractCodegenFileTest.java | 56 +++++++ .../javadoc/javadoc-examples.smithy | 48 ++++++ 7 files changed, 365 insertions(+) create mode 100644 codegen/plugins/client-codegen/src/main/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ClientJavadocExamplesIntegration.java create mode 100644 codegen/plugins/client-codegen/src/main/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ExamplesTraitInterceptor.java create mode 100644 codegen/plugins/client-codegen/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration create mode 100644 codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ClientJavadocExamplesIntegrationTest.java create mode 100644 codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/utils/AbstractCodegenFileTest.java create mode 100644 codegen/plugins/client-codegen/src/test/resources/software/amazon/smithy/java/codegen/client/integrations/javadoc/javadoc-examples.smithy diff --git a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/writer/JavaWriter.java b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/writer/JavaWriter.java index ebd14c127..e246a1577 100644 --- a/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/writer/JavaWriter.java +++ b/codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/writer/JavaWriter.java @@ -59,6 +59,20 @@ private void addImport(Symbol symbol) { addImport(symbol, symbol.getName()); } + /** + * @return Returns the namespace that the file being written is contained within. + */ + public String getPackageNamespace() { + return packageNamespace; + } + + /** + * @return Returns the name of the file being written to. + */ + public String getFilename() { + return filename; + } + @Override public String toString() { // Do not add headers or attempt symbol resolution for resource files diff --git a/codegen/plugins/client-codegen/src/main/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ClientJavadocExamplesIntegration.java b/codegen/plugins/client-codegen/src/main/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ClientJavadocExamplesIntegration.java new file mode 100644 index 000000000..1b1e854e4 --- /dev/null +++ b/codegen/plugins/client-codegen/src/main/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ClientJavadocExamplesIntegration.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.client.integrations.javadoc; + +import java.util.List; +import software.amazon.smithy.java.codegen.CodeGenerationContext; +import software.amazon.smithy.java.codegen.JavaCodegenIntegration; +import software.amazon.smithy.java.codegen.writer.JavaWriter; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds client examples to the generated Javadoc. + */ +@SmithyInternalApi +public class ClientJavadocExamplesIntegration implements JavaCodegenIntegration { + + @Override + public String name() { + return "client-javadoc-examples"; + } + + @Override + public List runBefore() { + // The DocumentationTrait interceptor uses "prepend", and the finalizing formatter + // wholly replaces the contents of the JavaDoc section with a formatted version. + // By running before the "javadoc" plugin, which includes those interceptors, + // we can be sure that what we write will appear after the docs from the doc trait + // and will still benefit from formatting. + return List.of("javadoc"); + } + + @Override + public List> interceptors( + CodeGenerationContext codegenContext + ) { + return List.of(new ExamplesTraitInterceptor(codegenContext)); + } + +} diff --git a/codegen/plugins/client-codegen/src/main/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ExamplesTraitInterceptor.java b/codegen/plugins/client-codegen/src/main/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ExamplesTraitInterceptor.java new file mode 100644 index 000000000..f8e04b832 --- /dev/null +++ b/codegen/plugins/client-codegen/src/main/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ExamplesTraitInterceptor.java @@ -0,0 +1,139 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.client.integrations.javadoc; + +import java.nio.file.Paths; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.java.codegen.CodeGenerationContext; +import software.amazon.smithy.java.codegen.CodegenUtils; +import software.amazon.smithy.java.codegen.generators.SnippetGenerator; +import software.amazon.smithy.java.codegen.sections.JavadocSection; +import software.amazon.smithy.java.codegen.writer.JavaWriter; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.traits.ExamplesTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.StringUtils; + +/** + * Adds Javadoc examples to operations with the examples trait. + */ +public class ExamplesTraitInterceptor implements CodeInterceptor.Appender { + + private final CodeGenerationContext context; + + public ExamplesTraitInterceptor(CodeGenerationContext context) { + this.context = context; + } + + @Override + public void append(JavaWriter writer, JavadocSection section) { + var operation = section.targetedShape() + .asOperationShape() + .orElseThrow(() -> new CodegenException(String.format( + "Expected shape to be an operation shape, but was " + section.targetedShape().getType()))); + var trait = section.targetedShape().expectTrait(ExamplesTrait.class); + writer.pushState(); + + var operationSymbol = context.symbolProvider().toSymbol(operation); + + // The effective heading levels are different if the documentation is being put + // in the operation's generated class docs or the client's generated method docs, + // so this checks to see which file we're in and adjusts the heading level + // accordingly. + var operationFile = Paths.get(operationSymbol.getDefinitionFile()).normalize(); + var activeFile = Paths.get(writer.getFilename()).normalize(); + if (operationFile.equals(activeFile)) { + writer.putContext("sectionHeading", "h2"); + writer.putContext("titleHeading", "h3"); + } else { + writer.putContext("sectionHeading", "h4"); + writer.putContext("titleHeading", "h5"); + } + + writer.write("<${sectionHeading:L}>Examples"); + for (ExamplesTrait.Example example : trait.getExamples()) { + writer.pushState(); + writer.putContext("docs", example.getDocumentation().orElse(null)); + writer.putContext("title", example.getTitle()); + writer.write(""" + <${titleHeading:L}>${title:L} + + ${?docs}

${docs:L} + ${/docs} +

+                    {@code
+                    ${C|}
+                    }
+                    
+ """, writer.consumer(w -> writeExampleSnippet(writer, operation, example))); + writer.popState(); + } + writer.popState(); + } + + // TODO: collect these and write them out to a shared snippets file + private void writeExampleSnippet(JavaWriter writer, OperationShape operation, ExamplesTrait.Example example) { + var service = context.model().expectShape(context.settings().service(), ServiceShape.class); + var operationName = StringUtils.uncapitalize(CodegenUtils.getDefaultName(operation, service)); + writer.putContext("operationName", operationName); + + var inputShape = context.model().expectShape(operation.getInputShape()); + writer.putContext( + "input", + SnippetGenerator.generateShapeInitializer(context, inputShape, example.getInput())); + + if (example.getOutput().isPresent() && !example.getOutput().get().isEmpty()) { + var outputShape = context.model().expectShape(operation.getOutputShape()); + writer.putContext( + "output", + SnippetGenerator.generateShapeInitializer(context, outputShape, example.getOutput().get())); + } else { + writer.putContext("output", null); + } + + if (example.getError().isPresent()) { + writer.putContext("hasError", true); + var error = example.getError().get(); + var errorShape = context.model().expectShape(error.getShapeId()); + writer.putContext( + "error", + SnippetGenerator.generateShapeInitializer(context, errorShape, error.getContent())); + writer.putContext("errorSymbol", context.symbolProvider().toSymbol(errorShape)); + } else { + writer.putContext("hasError", false); + } + + writer.writeInline(""" + var input = ${input:L|}; + ${?hasError} + + try { + client.${operationName:L}(input); + } catch (${errorSymbol:T} e) { + e.equals(${error:L|}); + } + ${/hasError} + ${^hasError} + + var result = client.${operationName:L}(input); + result.equals(${output:L|}); + ${/hasError}"""); + } + + @Override + public Class sectionType() { + return JavadocSection.class; + } + + @Override + public boolean isIntercepted(JavadocSection section) { + // The examples trait can only be applied to operations for now, + // but we add an explicit check anyway in case it ever gets expanded. + var shape = section.targetedShape(); + return shape.hasTrait(ExamplesTrait.class) && shape.isOperationShape(); + } +} diff --git a/codegen/plugins/client-codegen/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration b/codegen/plugins/client-codegen/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration new file mode 100644 index 000000000..c820c760d --- /dev/null +++ b/codegen/plugins/client-codegen/src/main/resources/META-INF/services/software.amazon.smithy.java.codegen.JavaCodegenIntegration @@ -0,0 +1 @@ +software.amazon.smithy.java.codegen.client.integrations.javadoc.ClientJavadocExamplesIntegration diff --git a/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ClientJavadocExamplesIntegrationTest.java b/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ClientJavadocExamplesIntegrationTest.java new file mode 100644 index 000000000..1af7bfa7b --- /dev/null +++ b/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/integrations/javadoc/ClientJavadocExamplesIntegrationTest.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.client.integrations.javadoc; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; + +import java.net.URL; +import java.util.Objects; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.java.codegen.client.utils.AbstractCodegenFileTest; + +public class ClientJavadocExamplesIntegrationTest extends AbstractCodegenFileTest { + private static final URL TEST_FILE = Objects.requireNonNull( + ClientJavadocExamplesIntegrationTest.class.getResource("javadoc-examples.smithy")); + + @Override + protected URL testFile() { + return TEST_FILE; + } + + @Test + void includesExamples() { + var fileContents = getFileStringForClass("client/TestServiceClient"); + var expected = """ + *

Examples

+ *
Basic Example
+ *
+                     * {@code
+                     * var input = ExamplesOperationInput.builder()
+                     *                 .foo("foo")
+                     *                 .build();
+                     *
+                     * var result = client.examplesOperation(input);
+                     * result.equals(ExamplesOperationOutput.builder()
+                     *                   .bar("bar")
+                     *                   .build());
+                     * }
+                     * 
+ * + *
Error Example
+ *
+                     * {@code
+                     * var input = ExamplesOperationInput.builder()
+                     *                 .foo("bar")
+                     *                 .build();
+                     *
+                     * try {
+                     *     client.examplesOperation(input);
+                     * } catch (ExampleError e) {
+                     *     e.equals(ExampleError.builder()
+                     *                  .message("bar")
+                     *                  .build());
+                     * }
+                     * }
+                     * 
+ """; + assertThat(fileContents, containsString(expected)); + } +} diff --git a/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/utils/AbstractCodegenFileTest.java b/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/utils/AbstractCodegenFileTest.java new file mode 100644 index 000000000..e108f6be2 --- /dev/null +++ b/codegen/plugins/client-codegen/src/test/java/software/amazon/smithy/java/codegen/client/utils/AbstractCodegenFileTest.java @@ -0,0 +1,56 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.codegen.client.utils; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URL; +import java.nio.file.Paths; +import org.junit.jupiter.api.BeforeEach; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.java.codegen.client.JavaClientCodegenPlugin; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.ObjectNode; + +public abstract class AbstractCodegenFileTest { + + protected final MockManifest manifest = new MockManifest(); + protected final SmithyBuildPlugin plugin = new JavaClientCodegenPlugin(); + + @BeforeEach + public void setup() { + var model = Model.assembler() + .addImport(testFile()) + .assemble() + .unwrap(); + var context = PluginContext.builder() + .fileManifest(manifest) + .settings(settings()) + .model(model) + .build(); + plugin.execute(context); + assertFalse(manifest.getFiles().isEmpty()); + } + + protected abstract URL testFile(); + + protected ObjectNode settings() { + return ObjectNode.builder() + .withMember("service", "smithy.java.codegen#TestService") + .withMember("namespace", "test.smithy.codegen") + .build(); + } + + protected String getFileStringForClass(String className) { + var fileStringOptional = manifest.getFileString( + Paths.get(String.format("/test/smithy/codegen/%s.java", className))); + assertTrue(fileStringOptional.isPresent()); + return fileStringOptional.get(); + } +} diff --git a/codegen/plugins/client-codegen/src/test/resources/software/amazon/smithy/java/codegen/client/integrations/javadoc/javadoc-examples.smithy b/codegen/plugins/client-codegen/src/test/resources/software/amazon/smithy/java/codegen/client/integrations/javadoc/javadoc-examples.smithy new file mode 100644 index 000000000..f88f3ca74 --- /dev/null +++ b/codegen/plugins/client-codegen/src/test/resources/software/amazon/smithy/java/codegen/client/integrations/javadoc/javadoc-examples.smithy @@ -0,0 +1,48 @@ +$version: "2" + +namespace smithy.java.codegen + +service TestService { + operations: [ + ExamplesOperation + ] +} + +@error("server") +structure ExampleError { + message: String +} + +/// Base docs +@examples([ + { + title: "Basic Example" + input: { + foo: "foo" + } + output: { + bar: "bar" + } + } + { + title: "Error Example" + input: { + foo: "bar" + } + error: { + shapeId: ExampleError + content: { + message: "bar" + } + } + } +]) +operation ExamplesOperation { + input := { + foo: String + } + output := { + bar: String + } + errors: [ExampleError] +}