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..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
@@ -82,6 +96,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
+}
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 extends CodeInterceptor extends CodeSection, JavaWriter>> 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${sectionHeading:L}>");
+ 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}${titleHeading: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]
+}