From dc6547e20af52ec5e9c0d871a763dcf607d48d3b Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Thu, 2 Oct 2025 11:14:12 +0200 Subject: [PATCH 1/6] Make DefaultBuilderRef public This makes `DefaultBuilderRef` public so that it can be sued for nested collections outside of smithy-utils. Nested collections can be quite tricky to handle properly, and having to re-write all the safe borrowing code for them shouldn't be necessary. --- .../feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json | 7 +++++++ .../software/amazon/smithy/utils/DefaultBuilderRef.java | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changes/next-release/feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json diff --git a/.changes/next-release/feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json b/.changes/next-release/feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json new file mode 100644 index 00000000000..698169e39f6 --- /dev/null +++ b/.changes/next-release/feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json @@ -0,0 +1,7 @@ +{ + "type": "feature", + "description": "Made `DefaultBuilderRef` public.", + "pull_requests": [ + "" + ] +} diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/DefaultBuilderRef.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/DefaultBuilderRef.java index 18886b101b2..cf8020ecc3c 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/DefaultBuilderRef.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/DefaultBuilderRef.java @@ -16,7 +16,7 @@ * * @param The type of value being built, borrowed, and copied. */ -final class DefaultBuilderRef implements BuilderRef { +public final class DefaultBuilderRef implements BuilderRef { private final Supplier ctor; private final Function copyCtor; @@ -26,7 +26,7 @@ final class DefaultBuilderRef implements BuilderRef { private T owned; private T borrowed; - DefaultBuilderRef( + public DefaultBuilderRef( Supplier ctor, Function copyCtor, Function immutableWrapper, From 5e14e39063df72bb5d36639e808e168b44a08c3f Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Wed, 8 Oct 2025 15:10:57 +0200 Subject: [PATCH 2/6] Add SnippetConfig to allow sharing doc snippets This adds a shared file definition for sharing generated snippets for things like the examples trait. These shared definitions will be consumed by smithy-docgen when generating docs. --- ...f8fb30971b29b76010e7143df11affcb9618f.json | 5 + .../smithy/codegen/core/docs/Snippet.java | 190 +++++++++++++ .../codegen/core/docs/SnippetConfig.java | 266 ++++++++++++++++++ .../smithy/codegen/core/docs/SnippetFile.java | 134 +++++++++ .../codegen/core/docs/SnippetConfigTest.java | 160 +++++++++++ .../codegen/core/docs/snippet-config.json | 49 ++++ 6 files changed, 804 insertions(+) create mode 100644 .changes/next-release/feature-df6f8fb30971b29b76010e7143df11affcb9618f.json create mode 100644 smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/Snippet.java create mode 100644 smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetConfig.java create mode 100644 smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetFile.java create mode 100644 smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/docs/SnippetConfigTest.java create mode 100644 smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/docs/snippet-config.json diff --git a/.changes/next-release/feature-df6f8fb30971b29b76010e7143df11affcb9618f.json b/.changes/next-release/feature-df6f8fb30971b29b76010e7143df11affcb9618f.json new file mode 100644 index 00000000000..f429655702c --- /dev/null +++ b/.changes/next-release/feature-df6f8fb30971b29b76010e7143df11affcb9618f.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "description": "Added a `SnippetConfig` class to codgen-core that allows sharing doc snippets that will be consumed by smithy-docgen.", + "pull_requests": [] +} diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/Snippet.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/Snippet.java new file mode 100644 index 00000000000..b91dce7d723 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/Snippet.java @@ -0,0 +1,190 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.codegen.core.docs; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.StringUtils; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Represents a generated snippet, potentially containing multiple logical files. + * + *

Snippets are generated code based on some trait or other shared definition, + * such as the {@link software.amazon.smithy.model.traits.ExamplesTrait}. These are + * created by code generators and consumed by documentation tools, such as + * smithy-docgen. + */ +@SmithyUnstableApi +public final class Snippet implements ToSmithyBuilder { + private final String targetId; + private final String title; + private final ShapeId protocol; + private final List files; + + private Snippet(Builder builder) { + this.targetId = SmithyBuilder.requiredState("targetId", builder.targetId); + if (StringUtils.isBlank(targetId)) { + throw new IllegalStateException("Snippet target id must not be blank"); + } + this.title = SmithyBuilder.requiredState("title", builder.title); + if (StringUtils.isBlank(title)) { + throw new IllegalStateException("Snippet title must not be blank"); + } + this.protocol = builder.protocol; + this.files = builder.files.copy(); + if (this.files.isEmpty()) { + throw new IllegalStateException("Snippets must contain at least one file"); + } + } + + /** + * Gets the identifier of what the snippet was generated for. + * + *

If this snippet represents a generated example from the + * {@link software.amazon.smithy.model.traits.ExamplesTrait}, this + * will be the title of the example. + * + * @return Returns the target identifier of the snippet. + */ + public String getTargetId() { + return targetId; + } + + /** + * Gets the title of the snippet. + * + *

This is distinct from the target ID as it identifies the specific snippet. + * + *

Generally, the title should reflect the primary language of the snippet, + * such as "Python" for a Python snippet. + * + * @return Returns the title of the snippet. + */ + public String getTitle() { + return title; + } + + /** + * @return Optionally returns the ShapeId of the protocol associated with this snippet. + */ + public Optional getProtocol() { + return Optional.ofNullable(protocol); + } + + /** + * @return Returns the files that comprise the snippet. + */ + public List getFiles() { + return files; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Snippet)) { + return false; + } + Snippet snippet = (Snippet) o; + return Objects.equals(targetId, snippet.targetId) + && Objects.equals(protocol, snippet.protocol) + && Objects.equals(files, snippet.files); + } + + @Override + public int hashCode() { + return Objects.hash(targetId, protocol, files); + } + + @Override + public Builder toBuilder() { + return builder() + .targetId(targetId) + .protocol(protocol) + .files(files); + } + + /** + * @return Returns a new Snippet builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder implements SmithyBuilder { + private String targetId; + private String title; + private ShapeId protocol; + private final BuilderRef> files = BuilderRef.forList(); + + @Override + public Snippet build() { + return new Snippet(this); + } + + /** + * Sets the target id of the snippet. + * + * @param id The id to set as the target of the snippet. + * @return Returns the builder. + */ + public Builder targetId(String id) { + this.targetId = id; + return this; + } + + /** + * Sets the title of the snippet. + * + * @param title The title to set for the snippet. + * @return Returns the builder. + */ + public Builder title(String title) { + this.title = title; + return this; + } + + /** + * Sets the protocol of the snippet. + * + *

To remove the protocol, set a null value. + * + * @param protocol The shape id of the protocol the snippet is tied to. + * @return Returns the builder. + */ + public Builder protocol(ShapeId protocol) { + this.protocol = protocol; + return this; + } + + /** + * Sets the files that make up the snippet. + * + * @param files A list of files that make up the snippet. + * @return Returns the builder. + */ + public Builder files(Collection files) { + this.files.clear(); + this.files.get().addAll(files); + return this; + } + + /** + * Adds a file to the snippet. + * + * @param file A file to add to the snippet. + * @return Returns the builder. + */ + public Builder addFile(SnippetFile file) { + this.files.get().add(file); + return this; + } + } +} diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetConfig.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetConfig.java new file mode 100644 index 00000000000..9a3eca82035 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetConfig.java @@ -0,0 +1,266 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.codegen.core.docs; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.NodeMapper; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.ToNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.BuilderRef; +import software.amazon.smithy.utils.DefaultBuilderRef; +import software.amazon.smithy.utils.IoUtils; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Represents a file containing generated snippets. + * + *

Snippets are generated code based on some trait or other shared definition, + * such as the {@link software.amazon.smithy.model.traits.ExamplesTrait}. These are + * created by code generators and consumed by documentation tools, such as + * smithy-docgen. + * + *

These are differentiated from typical code gen artifacts in that they are + * intended to be shared. These may be distributed and aggregated in any manner, + * but they SHOULD be written to the {@literal snippets} directory of the shared + * plugin space by Smithy build plugins. smithy-docgen will discover and include + * any snippet files in that directory. Smithy build plugins MUST declare a + * {@code runBefore} on "docgen" for their generated snippets to be included. + */ +@SmithyUnstableApi +public final class SnippetConfig implements ToSmithyBuilder, ToNode { + public static final String DEFAULT_VERSION = "1.0"; + + private final String version; + private final Map>> snippets; + + private SnippetConfig(Builder builder) { + this.version = SmithyBuilder.requiredState("version", builder.version); + this.snippets = builder.snippets.copy(); + } + + /** + * @return Returns the version of the snippet config. + */ + public String getVersion() { + return version; + } + + /** + * @return Returns all the snippets in the config. + */ + public Map>> getSnippets() { + return snippets; + } + + /** + * Gets all the snippets for a particular service. + * + * @param id The id of the service to get snippets for. + * @return Returns the snippets for shapes bound to the service. + */ + public Map> getServiceSnippets(ShapeId id) { + return snippets.getOrDefault(id, Collections.emptyMap()); + } + + /** + * Gets the snippets for a particular shape in a service. + * + * @param serviceId The service to search for snippets. + * @param shapeId The shape to get snippets for. + * @return Returns the snippets for a shape from a service. + */ + public List getShapeSnippets(ShapeId serviceId, ShapeId shapeId) { + return getServiceSnippets(serviceId).getOrDefault(shapeId, Collections.emptyList()); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SnippetConfig)) { + return false; + } + SnippetConfig config = (SnippetConfig) o; + return Objects.equals(version, config.version) && Objects.equals(snippets, config.snippets); + } + + @Override + public int hashCode() { + return Objects.hash(version, snippets); + } + + @Override + public Builder toBuilder() { + return builder() + .version(version) + .snippets(snippets); + } + + /** + * @return Returns a new SnippetConfig builder. + */ + public static Builder builder() { + return new Builder(); + } + + @Override + public Node toNode() { + return ObjectNode.objectNodeBuilder() + .withMember("version", version) + .withMember("snippets", new NodeMapper().serialize(snippets)) + .build(); + } + + /** + * @param node A node representing a SnippetConfig. + * @return Returns a SnippetConfig based on the given node. + */ + public static SnippetConfig fromNode(Node node) { + Builder builder = builder(); + new NodeMapper().deserializeInto(node, builder); + return builder.build(); + } + + /** + * Loads a SnippetConfig from a JSON file. + * + * @param path The path to the JSON snippet config. + * @return Returns a SnippetConfig based on the given file. + */ + public static SnippetConfig load(Path path) { + return fromNode(Node.parse(IoUtils.readUtf8File(path))); + } + + public final static class Builder implements SmithyBuilder { + private String version = DEFAULT_VERSION; + private final BuilderRef>>> snippets = new DefaultBuilderRef<>( + LinkedHashMap::new, + Builder::copySnippetMap, + Builder::immutableWrapSnippetMap, + Collections::emptyMap); + + private static Map>> copySnippetMap( + Map>> snippets + ) { + LinkedHashMap>> copy = new LinkedHashMap<>(); + for (Map.Entry>> entry : snippets.entrySet()) { + LinkedHashMap> serviceCopy = new LinkedHashMap<>(); + for (Map.Entry> serviceEntry : entry.getValue().entrySet()) { + serviceCopy.put(serviceEntry.getKey(), new ArrayList<>(serviceEntry.getValue())); + } + copy.put(entry.getKey(), serviceCopy); + } + return copy; + } + + private static Map>> immutableWrapSnippetMap( + Map>> snippets + ) { + LinkedHashMap>> copy = new LinkedHashMap<>(); + for (Map.Entry>> entry : snippets.entrySet()) { + LinkedHashMap> serviceCopy = new LinkedHashMap<>(); + for (Map.Entry> serviceEntry : entry.getValue().entrySet()) { + serviceCopy.put(serviceEntry.getKey(), ListUtils.copyOf(serviceEntry.getValue())); + } + copy.put(entry.getKey(), MapUtils.orderedCopyOf(serviceCopy)); + } + return MapUtils.orderedCopyOf(copy); + } + + @Override + public SnippetConfig build() { + return new SnippetConfig(this); + } + + /** + * Sets the version of the config schema. + * + * @param version The version to set. + * @return Returns the builder. + */ + public Builder version(String version) { + this.version = version; + return this; + } + + /** + * Sets the snippet map, erasing any existing values. + * + * @param snippets The snippets to set. + * @return Returns the builder. + */ + public Builder snippets(Map>> snippets) { + this.snippets.clear(); + for (Map.Entry>> entry : snippets.entrySet()) { + putServiceSnippets(entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * Merges new snippets into the snippet map. + * + * @param snippets The snippets to merge in. + * @return Returns the builder. + */ + public Builder mergeSnippets(Map>> snippets) { + for (Map.Entry>> entry : snippets.entrySet()) { + mergeServiceSnippets(entry.getKey(), entry.getValue()); + } + return this; + } + + public Builder mergeSnippets(SnippetConfig snippetConfig) { + if (!snippetConfig.getVersion().equals(version)) { + throw new IllegalArgumentException(String.format( + "Tried to merge snippets from incompatible version. Expected: %s but was %s", + version, + snippetConfig.getVersion())); + } + return mergeSnippets(snippetConfig.getSnippets()); + } + + /** + * Sets the snippets for a particular service, erasing any existing values. + * + * @param serviceId The service to set the snippets for. + * @param serviceSnippets The snippets to set in the service. + * @return Returns the builder. + */ + public Builder putServiceSnippets(ShapeId serviceId, Map> serviceSnippets) { + Map> mutableCopy = new LinkedHashMap<>(serviceSnippets.size()); + for (Map.Entry> entry : serviceSnippets.entrySet()) { + mutableCopy.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + snippets.get().put(serviceId, mutableCopy); + return this; + } + + /** + * Merges in the snippets for a particular service. + * + * @param serviceId The service to merge snippets for. + * @param serviceSnippets The snippets to merge in. + * @return Returns the builder. + */ + public Builder mergeServiceSnippets(ShapeId serviceId, Map> serviceSnippets) { + Map> old = snippets.get().computeIfAbsent(serviceId, k -> new LinkedHashMap<>()); + for (Map.Entry> entry : serviceSnippets.entrySet()) { + old.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()).addAll(entry.getValue()); + } + return this; + } + } +} diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetFile.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetFile.java new file mode 100644 index 00000000000..a165eb3ce09 --- /dev/null +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetFile.java @@ -0,0 +1,134 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.codegen.core.docs; + +import java.util.Objects; +import software.amazon.smithy.utils.SmithyBuilder; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.StringUtils; +import software.amazon.smithy.utils.ToSmithyBuilder; + +/** + * Represents a single file that is part of a snippet. This could be a config file, a source code file, + * or some logically differentiated file type. + */ +@SmithyUnstableApi +public class SnippetFile implements ToSmithyBuilder { + private final String language; + private final String filename; + private final String content; + + private SnippetFile(Builder builder) { + this.language = SmithyBuilder.requiredState("language", builder.language); + if (StringUtils.isBlank(language)) { + throw new IllegalStateException("SnippetFile language must not be blank"); + } + this.filename = SmithyBuilder.requiredState("filename", builder.filename); + if (StringUtils.isBlank(filename)) { + throw new IllegalStateException("SnippetFile filename must not be blank"); + } + this.content = SmithyBuilder.requiredState("content", builder.content); + } + + /** + * Gets the language used by the file. + * + *

This will be used for syntax highlighting. + * + * @return Returns a string representation of the file's language. + */ + public String getLanguage() { + return language; + } + + /** + * @return Returns the name of the file. + */ + public String getFilename() { + return filename; + } + + /** + * @return Returns the content of the file. + */ + public String getContent() { + return content; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SnippetFile)) { + return false; + } + SnippetFile that = (SnippetFile) o; + return Objects.equals(language, that.language) + && Objects.equals(filename, that.filename) + && Objects.equals(content, that.content); + } + + @Override + public int hashCode() { + return Objects.hash(language, filename, content); + } + + @Override + public Builder toBuilder() { + return builder() + .language(language) + .filename(filename) + .content(content); + } + + /** + * @return Returns a new SnippetFile builder. + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder implements SmithyBuilder { + private String language; + private String filename; + private String content; + + @Override + public SnippetFile build() { + return new SnippetFile(this); + } + + /** + * Sets the language of the snippet file. + * + * @param language The language of the snippet file. + * @return Returns the builder. + */ + public Builder language(String language) { + this.language = language; + return this; + } + + /** + * Sets the name of the snippet file. + * + * @param filename The file name of the snippet. + * @return Returns the builder. + */ + public Builder filename(String filename) { + this.filename = filename; + return this; + } + + /** + * Sets the content of the snippet file. + * + * @param content The snippet file contents. + * @return Returns the builder. + */ + public Builder content(String content) { + this.content = content; + return this; + } + } +} diff --git a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/docs/SnippetConfigTest.java b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/docs/SnippetConfigTest.java new file mode 100644 index 00000000000..682b2526845 --- /dev/null +++ b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/docs/SnippetConfigTest.java @@ -0,0 +1,160 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.codegen.core.docs; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.file.Paths; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.ListUtils; +import software.amazon.smithy.utils.MapUtils; + +public class SnippetConfigTest { + private static final ShapeId serviceId = ShapeId.from("com.example#BirdService"); + private static final ShapeId operationId = ShapeId.from("com.example#ListBirds"); + private static SnippetConfig snippetConfig; + + @BeforeAll + public static void setup() { + String id = "List Crows"; + Snippet pythonSnippet = Snippet.builder() + .targetId(id) + .title("Python") + .addFile(SnippetFile.builder() + .language("python") + .filename("main.py") + .content( + "from bird_service import BirdClient\n\n" + + "client = BirdClient()\n" + + "response = await client.list_birds(ListBirdsInput(genus=\"corvus\"))\n" + + "assert response == ListBirdsOutput(birds=[\n" + + " Bird(\n" + + " order=\"passiformes\",\n" + + " family=\"corvidae\",\n" + + " genus=\"corvus\",\n" + + " species=\"cornix\",\n" + + " common_names={\n" + + " \"en_US\": [\"Hooded Crow\"],\n" + + " \"de_DE\": [\"Nebelkrähe\"],\n" + + " },\n" + + " ),\n" + + "])") + .build()) + .build(); + Snippet pythonProtocolSnippet = Snippet.builder() + .targetId(id) + .title("Python (restJson1)") + .protocol(ShapeId.from("aws.protocols#restJson1")) + .addFile(SnippetFile.builder() + .language("python") + .filename("main.py") + .content( + "from bird_service import BirdClient\n\n" + + "client = BirdClient(protocol=RestJson1Protocol())\n" + + "response = await client.list_birds(ListBirdsInput(genus=\"corvus\"))\n" + + "assert response == ListBirdsOutput(birds=[\n" + + " Bird(\n" + + " order=\"passiformes\",\n" + + " family=\"corvidae\",\n" + + " genus=\"corvus\",\n" + + " species=\"cornix\",\n" + + " common_names={\n" + + " \"en_US\": [\"Hooded Crow\"],\n" + + " \"de_DE\": [\"Nebelkrähe\"],\n" + + " },\n" + + " ),\n" + + "])") + .build()) + .build(); + Snippet httpSnippet = Snippet.builder() + .targetId(id) + .title("HTTP") + .protocol(ShapeId.from("aws.protocols#restJson1")) + .addFile(SnippetFile.builder() + .language("http") + .filename("request") + .content( + "POST /birds HTTP/1.1\n" + + "Host: com.example.birds\n" + + "Content-Type: application/json\n" + + "Content-Length: 25\n\n" + + "{\n" + + " \"genus\": \"corvus\"\n" + + "}") + .build()) + .addFile(SnippetFile.builder() + .language("http") + .filename("response") + .content( + "HTTP/1.1 200 OK\n" + + "Content-Type: application/json\n" + + "Content-Length: 248\n\n" + + "{\n" + + " \"birds\": [{\n" + + " \"order\": \"passiformes\"\n" + + " \"family\": \"corvidae\"\n" + + " \"genus\": \"corvus\"\n" + + " \"species\": \"cornix\"\n" + + " \"commonNames\": {\n" + + " \"en_US\": [\"Hooded Crow\"]\n" + + " \"de_DE\": [\"Nebelkrähe\"]\n" + + " }\n" + + " }]\n" + + "}") + .build()) + .build(); + List snippets = ListUtils.of(pythonSnippet, pythonProtocolSnippet, httpSnippet); + snippetConfig = SnippetConfig.builder() + .putServiceSnippets(serviceId, MapUtils.of(operationId, snippets)) + .build(); + } + + @Test + public void serializesToNode() throws Exception { + Node result = snippetConfig.toNode(); + Node expected = Node.parse(getClass().getResource("snippet-config.json").openStream()); + assertEquals(result, expected); + } + + @Test + public void parsesNode() throws Exception { + SnippetConfig actual = SnippetConfig.load(Paths.get(getClass().getResource("snippet-config.json").toURI())); + assertEquals(actual, snippetConfig); + } + + @Test + public void roundTrips() throws Exception { + Node expected = Node.parse(getClass().getResource("snippet-config.json").openStream()); + Node actual = SnippetConfig.fromNode(expected).toNode(); + Node.assertEquals(actual, expected); + } + + @Test + public void canMergeNewSnippets() { + Snippet newSnippet = Snippet.builder() + .targetId("newSnippet") + .title("newSnippet") + .addFile(SnippetFile.builder() + .language("txt") + .filename("newSnippet.txt") + .content("foo") + .build()) + .build(); + SnippetConfig merged = snippetConfig.toBuilder() + .mergeSnippets(MapUtils.of(serviceId, MapUtils.of(operationId, ListUtils.of(newSnippet)))) + .build(); + + List oldList = snippetConfig.getShapeSnippets(serviceId, operationId); + List newList = merged.getShapeSnippets(serviceId, operationId); + for (int i = 0; i < oldList.size(); i++) { + assertEquals(oldList.get(i), newList.get(i)); + } + assertEquals(newList.get(newList.size() - 1), newSnippet); + } +} diff --git a/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/docs/snippet-config.json b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/docs/snippet-config.json new file mode 100644 index 00000000000..6422bd84321 --- /dev/null +++ b/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/docs/snippet-config.json @@ -0,0 +1,49 @@ +{ + "version": "1.0", + "snippets": { + "com.example#BirdService": { + "com.example#ListBirds": [ + { + "targetId": "List Crows", + "title": "Python", + "files": [ + { + "language": "python", + "filename": "main.py", + "content": "from bird_service import BirdClient\n\nclient = BirdClient()\nresponse = await client.list_birds(ListBirdsInput(genus=\"corvus\"))\nassert response == ListBirdsOutput(birds=[\n Bird(\n order=\"passiformes\",\n family=\"corvidae\",\n genus=\"corvus\",\n species=\"cornix\",\n common_names={\n \"en_US\": [\"Hooded Crow\"],\n \"de_DE\": [\"Nebelkrähe\"],\n },\n ),\n])" + } + ] + }, + { + "targetId": "List Crows", + "title": "Python (restJson1)", + "protocol": "aws.protocols#restJson1", + "files": [ + { + "language": "python", + "filename": "main.py", + "content": "from bird_service import BirdClient\n\nclient = BirdClient(protocol=RestJson1Protocol())\nresponse = await client.list_birds(ListBirdsInput(genus=\"corvus\"))\nassert response == ListBirdsOutput(birds=[\n Bird(\n order=\"passiformes\",\n family=\"corvidae\",\n genus=\"corvus\",\n species=\"cornix\",\n common_names={\n \"en_US\": [\"Hooded Crow\"],\n \"de_DE\": [\"Nebelkrähe\"],\n },\n ),\n])" + } + ] + }, + { + "targetId": "List Crows", + "title": "HTTP", + "protocol": "aws.protocols#restJson1", + "files": [ + { + "language": "http", + "filename": "request", + "content": "POST /birds HTTP/1.1\nHost: com.example.birds\nContent-Type: application/json\nContent-Length: 25\n\n{\n \"genus\": \"corvus\"\n}" + }, + { + "language": "http", + "filename": "response", + "content": "HTTP/1.1 200 OK\nContent-Type: application/json\nContent-Length: 248\n\n{\n \"birds\": [{\n \"order\": \"passiformes\"\n \"family\": \"corvidae\"\n \"genus\": \"corvus\"\n \"species\": \"cornix\"\n \"commonNames\": {\n \"en_US\": [\"Hooded Crow\"]\n \"de_DE\": [\"Nebelkrähe\"]\n }\n }]\n}" + } + ] + } + ] + } + } +} From 3fa5543a79dd5d2f57579be07442a69a86080f5b Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Tue, 16 Dec 2025 13:43:53 +0100 Subject: [PATCH 3/6] Load snippets from snippet files in docgen This updates smithy-docgen to load snippets from either shared snippet files or explicitly configured snippet files. These are used to fill out the examples from the examples trait. --- ...0351cce599e85b810d4418fee8e5ca661436e.json | 5 + .../amazon/smithy/docgen/PluginTest.java | 42 ++++-- .../software/amazon/smithy/docgen/main.smithy | 5 - .../amazon/smithy/docgen/snippets.json | 25 ++++ .../amazon/smithy/docgen/DirectedDocGen.java | 40 ++++- .../smithy/docgen/DocGenerationContext.java | 31 ++++ .../amazon/smithy/docgen/DocSettings.java | 17 ++- .../amazon/smithy/docgen/SmithyDocPlugin.java | 1 + .../docgen/generators/OperationGenerator.java | 73 +++++++-- .../smithy/docgen/SmithyDocPluginTest.java | 49 +++---- .../generators/OperationGeneratorTest.java | 138 ++++++++++++++++++ .../docgen/utils/AbstractDocGenFileTest.java | 63 ++++++++ .../generators/operation-generator.smithy | 85 +++++++++++ .../smithy/docgen/generators/snippets.json | 53 +++++++ 14 files changed, 566 insertions(+), 61 deletions(-) create mode 100644 .changes/next-release/feature-3c70351cce599e85b810d4418fee8e5ca661436e.json create mode 100644 smithy-docgen/src/it/resources/software/amazon/smithy/docgen/snippets.json create mode 100644 smithy-docgen/src/test/java/software/amazon/smithy/docgen/generators/OperationGeneratorTest.java create mode 100644 smithy-docgen/src/test/java/software/amazon/smithy/docgen/utils/AbstractDocGenFileTest.java create mode 100644 smithy-docgen/src/test/resources/software/amazon/smithy/docgen/generators/operation-generator.smithy create mode 100644 smithy-docgen/src/test/resources/software/amazon/smithy/docgen/generators/snippets.json diff --git a/.changes/next-release/feature-3c70351cce599e85b810d4418fee8e5ca661436e.json b/.changes/next-release/feature-3c70351cce599e85b810d4418fee8e5ca661436e.json new file mode 100644 index 00000000000..c375cd3ee6c --- /dev/null +++ b/.changes/next-release/feature-3c70351cce599e85b810d4418fee8e5ca661436e.json @@ -0,0 +1,5 @@ +{ + "type": "feature", + "description": "Updated smithy-docgen to consume snippet files to generate example sections.", + "pull_requests": [] +} diff --git a/smithy-docgen/src/it/java/software/amazon/smithy/docgen/PluginTest.java b/smithy-docgen/src/it/java/software/amazon/smithy/docgen/PluginTest.java index a50cc9a03f5..79dbf9b4efd 100644 --- a/smithy-docgen/src/it/java/software/amazon/smithy/docgen/PluginTest.java +++ b/smithy-docgen/src/it/java/software/amazon/smithy/docgen/PluginTest.java @@ -7,11 +7,15 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.URL; import java.nio.file.Path; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import software.amazon.smithy.build.MockManifest; +import org.junit.jupiter.api.io.TempDir; +import software.amazon.smithy.build.FileManifest; import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.build.SmithyBuildPlugin; import software.amazon.smithy.model.Model; @@ -20,13 +24,17 @@ public class PluginTest { - private static MockManifest manifest; + private static final URL SNIPPETS = Objects.requireNonNull(PluginTest.class.getResource("snippets.json")); + + private static FileManifest manifest; + private static FileManifest sharedManifest; private static Model model; private static SmithyBuildPlugin plugin; @BeforeAll - public static void setup() { - manifest = new MockManifest(); + public static void setup(@TempDir Path tempDir) { + manifest = FileManifest.create(tempDir.resolve("main")); + sharedManifest = FileManifest.create(tempDir.resolve("shared")); model = getModel("main.smithy"); plugin = new SmithyDocPlugin(); } @@ -41,18 +49,22 @@ public void pluginGeneratesMarkdown() { Node.objectNodeBuilder() .withMember("com.example#ExternalResource", "https://aws.amazon.com") .build()) + .withMember("snippetConfigs", Node.fromStrings(SNIPPETS.getFile())) .build(); PluginContext context = getPluginContext(model, settings); plugin.execute(context); assertFalse(manifest.getFiles().isEmpty()); - Set files = manifest.getFiles(); - assertTrue(files.contains(Path.of("/content", "index.md"))); + Set files = manifest.getFiles() + .stream() + .map(path -> manifest.getBaseDir().relativize(path)) + .collect(Collectors.toSet()); + assertTrue(files.contains(Path.of("content", "index.md"))); } @Test - public void pluginGeneratesSphinxMarkdown() { + public void pluginGeneratesSphinxMarkdown(@TempDir Path tempDir) { Model model = getModel("main.smithy"); ObjectNode settings = Node.objectNodeBuilder() .withMember("service", "com.example#DocumentedService") @@ -62,19 +74,22 @@ public void pluginGeneratesSphinxMarkdown() { Node.objectNodeBuilder() .withMember("com.example#ExternalResource", "https://aws.amazon.com") .build()) + .withMember("snippetConfigs", Node.fromStrings(SNIPPETS.getFile())) .build(); PluginContext context = getPluginContext(model, settings); plugin.execute(context); - assertFalse(manifest.getFiles().isEmpty()); - Set files = manifest.getFiles(); - assertTrue(files.contains(Path.of("/requirements.txt"))); - assertTrue(files.contains(Path.of("/content", "conf.py"))); - assertTrue(files.contains(Path.of("/content", "index.md"))); + Set files = manifest.getFiles() + .stream() + .map(path -> manifest.getBaseDir().relativize(path)) + .collect(Collectors.toSet()); + assertTrue(files.contains(Path.of("requirements.txt"))); + assertTrue(files.contains(Path.of("content", "conf.py"))); + assertTrue(files.contains(Path.of("content", "index.md"))); // Assert that the transform to upgrade enum strings is applied. - assertTrue(files.contains(Path.of("/content", "shapes", "LegacyEnumTrait.md"))); + assertTrue(files.contains(Path.of("content", "shapes", "LegacyEnumTrait.md"))); } private static Model getModel(String path) { @@ -88,6 +103,7 @@ private static Model getModel(String path) { private static PluginContext getPluginContext(Model model, ObjectNode settings) { return PluginContext.builder() .fileManifest(manifest) + .sharedFileManifest(sharedManifest) .model(model) .settings(settings) .build(); diff --git a/smithy-docgen/src/it/resources/software/amazon/smithy/docgen/main.smithy b/smithy-docgen/src/it/resources/software/amazon/smithy/docgen/main.smithy index 561d9181787..6be37d6732f 100644 --- a/smithy-docgen/src/it/resources/software/amazon/smithy/docgen/main.smithy +++ b/smithy-docgen/src/it/resources/software/amazon/smithy/docgen/main.smithy @@ -278,11 +278,6 @@ structure SimpleError with [ErrorMixin] {} /// This operation showcases the various /// [documentation traits](https://smithy.io/2.0/spec/documentation-traits.html). /// -/// Note that examples are only half-supported right now. An interface needs -/// to be created and implemented to allow both code generators and protocols -/// to provide examples that will be used in those sections. For now, it just -/// shows the raw input / output structures. -/// /// The `title` trait is applied to the service. @examples([ { diff --git a/smithy-docgen/src/it/resources/software/amazon/smithy/docgen/snippets.json b/smithy-docgen/src/it/resources/software/amazon/smithy/docgen/snippets.json new file mode 100644 index 00000000000..54f82039fdb --- /dev/null +++ b/smithy-docgen/src/it/resources/software/amazon/smithy/docgen/snippets.json @@ -0,0 +1,25 @@ +{ + "version": "1.0", + "snippets": { + "com.example#DocumentedService": { + "com.example#DocumentationTraits": [ + { + "targetId": "Basic Example", + "title": "HTTP (restJson1)", + "files": [ + { + "language": "http", + "filename": "request", + "content": "POST /DocumentationTraits HTTP/1.1\nHost: com.example\nContent-Type: application/json\nContent-Length: 19\n\n{\"deprecated\":\"foo\"}" + }, + { + "language": "http", + "filename": "response", + "content": "foo" + } + ] + } + ] + } + } +} diff --git a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DirectedDocGen.java b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DirectedDocGen.java index 0fc0637fbbc..eb495c0fde8 100644 --- a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DirectedDocGen.java +++ b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DirectedDocGen.java @@ -4,6 +4,11 @@ */ package software.amazon.smithy.docgen; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.directed.CreateContextDirective; import software.amazon.smithy.codegen.core.directed.CreateSymbolProviderDirective; @@ -16,6 +21,7 @@ import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective; import software.amazon.smithy.codegen.core.directed.GenerateStructureDirective; import software.amazon.smithy.codegen.core.directed.GenerateUnionDirective; +import software.amazon.smithy.codegen.core.docs.SnippetConfig; import software.amazon.smithy.docgen.generators.MemberGenerator.MemberListingType; import software.amazon.smithy.docgen.generators.OperationGenerator; import software.amazon.smithy.docgen.generators.ResourceGenerator; @@ -31,6 +37,7 @@ */ @SmithyUnstableApi final class DirectedDocGen implements DirectedCodegen { + private static final Logger LOGGER = Logger.getLogger(DirectedDocGen.class.getName()); @Override public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective directive) { @@ -44,7 +51,38 @@ public DocGenerationContext createContext(CreateContextDirective directive) { + var settings = directive.settings(); + var sharedFileManifest = directive.sharedFileManifest(); + Set snippetConfigs = new LinkedHashSet<>(settings.snippetConfigs()); + + if (sharedFileManifest.isPresent()) { + var snippetsDir = sharedFileManifest.get().getBaseDir().resolve("snippets"); + sharedFileManifest.get() + .getFiles() + .stream() + .filter(path -> path.startsWith(snippetsDir) && path.toString().endsWith(".json")) + .forEach(snippetConfigs::add); + } + + if (snippetConfigs.isEmpty()) { + LOGGER.fine(() -> "No snippet configs were provided or discovered."); + } else { + LOGGER.fine(() -> { + String configFiles = snippetConfigs.stream().map(Path::toString).collect(Collectors.joining(", ")); + return String.format("Loading snippet config(s) from: %s", configFiles); + }); + } + + SnippetConfig.Builder builder = SnippetConfig.builder(); + for (Path snippetConfig : snippetConfigs) { + builder.mergeSnippets(SnippetConfig.load(snippetConfig)); + } + return builder.build(); } @Override diff --git a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocGenerationContext.java b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocGenerationContext.java index 377c5507a31..9dc8ec6c980 100644 --- a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocGenerationContext.java +++ b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocGenerationContext.java @@ -11,6 +11,7 @@ import software.amazon.smithy.codegen.core.CodegenException; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.WriterDelegator; +import software.amazon.smithy.codegen.core.docs.SnippetConfig; import software.amazon.smithy.docgen.DocSymbolProvider.FileExtensionDecorator; import software.amazon.smithy.docgen.writers.DocWriter; import software.amazon.smithy.model.Model; @@ -29,6 +30,7 @@ public final class DocGenerationContext implements CodegenContext writerDelegator; private final List docIntegrations; private final DocFormat docFormat; + private final SnippetConfig snippetConfig; /** * Constructor. @@ -45,11 +47,33 @@ public DocGenerationContext( SymbolProvider symbolProvider, FileManifest fileManifest, List docIntegrations + ) { + this(model, docSettings, symbolProvider, fileManifest, docIntegrations, null); + } + + /** + * Constructor. + * + * @param model The source model to generate for. + * @param docSettings Settings to customize generation. + * @param symbolProvider The symbol provider to use to turn shapes into symbols. + * @param fileManifest The file manifest to write to. + * @param docIntegrations A list of integrations to apply during generation. + * @param snippetConfig A loaded snippet file to include in the docs. + */ + public DocGenerationContext( + Model model, + DocSettings docSettings, + SymbolProvider symbolProvider, + FileManifest fileManifest, + List docIntegrations, + SnippetConfig snippetConfig ) { this.model = model; this.docSettings = docSettings; this.fileManifest = fileManifest; this.docIntegrations = docIntegrations; + this.snippetConfig = snippetConfig; DocFormat resolvedFormat = null; var availableFormats = new LinkedHashSet(); @@ -111,4 +135,11 @@ public List integrations() { public DocFormat docFormat() { return this.docFormat; } + + /** + * @return Returns the optional, consolidated snippet config. + */ + public SnippetConfig snippetConfig() { + return snippetConfig; + } } diff --git a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocSettings.java b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocSettings.java index 40035701572..97e41d9e567 100644 --- a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocSettings.java +++ b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocSettings.java @@ -4,9 +4,13 @@ */ package software.amazon.smithy.docgen; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; +import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.utils.SmithyUnstableApi; @@ -21,9 +25,10 @@ * when generating links for the * references trait * for resources that are not contained within the model. + * @param snippetConfigs A list of files containing snippets to include when generating docs. */ @SmithyUnstableApi -public record DocSettings(ShapeId service, String format, Map references) { +public record DocSettings(ShapeId service, String format, Map references, List snippetConfigs) { /** * Settings for documentation generation. These can be set in the @@ -37,6 +42,10 @@ public record DocSettings(ShapeId service, String format, Map r Objects.requireNonNull(format); } + public DocSettings(ShapeId service, String format, Map references) { + this(service, format, references, List.of()); + } + /** * Load the settings from an {@code ObjectNode}. * @@ -52,9 +61,13 @@ public static DocSettings fromNode(ObjectNode pluginSettings) { .collect(Collectors.toMap( e -> ShapeId.from(e.getKey().getValue()), e -> e.getValue().expectStringNode().getValue())); + var snippetConfigs = pluginSettings.getArrayMember("snippetConfigs") + .orElse(Node.arrayNode()) + .getElementsAs(e -> Paths.get(e.expectStringNode().getValue())); return new DocSettings( pluginSettings.expectStringMember("service").expectShapeId(), pluginSettings.getStringMemberOrDefault("format", "sphinx-markdown"), - references); + references, + snippetConfigs); } } diff --git a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/SmithyDocPlugin.java b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/SmithyDocPlugin.java index 8162fdbb3b1..77a2cac19eb 100644 --- a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/SmithyDocPlugin.java +++ b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/SmithyDocPlugin.java @@ -42,6 +42,7 @@ public void execute(PluginContext pluginContext) { runner.directedCodegen(new DirectedDocGen()); runner.integrationClass(DocIntegration.class); runner.fileManifest(pluginContext.getFileManifest()); + runner.sharedFileManifest(pluginContext.getSharedFileManifest()); runner.model(getValidatedModel(pluginContext.getModel()).unwrap()); DocSettings settings = runner.settings(DocSettings.class, pluginContext.getSettings()); runner.service(settings.service()); diff --git a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/generators/OperationGenerator.java b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/generators/OperationGenerator.java index 655b16357a0..2a19e5f0356 100644 --- a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/generators/OperationGenerator.java +++ b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/generators/OperationGenerator.java @@ -4,10 +4,16 @@ */ package software.amazon.smithy.docgen.generators; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.function.Consumer; +import java.util.stream.Collectors; import software.amazon.smithy.codegen.core.directed.GenerateOperationDirective; +import software.amazon.smithy.codegen.core.docs.Snippet; +import software.amazon.smithy.codegen.core.docs.SnippetFile; import software.amazon.smithy.docgen.DocFormat; import software.amazon.smithy.docgen.DocGenerationContext; import software.amazon.smithy.docgen.DocIntegration; @@ -26,7 +32,6 @@ import software.amazon.smithy.docgen.sections.ShapeSubheadingSection; import software.amazon.smithy.docgen.writers.DocWriter; import software.amazon.smithy.docgen.writers.DocWriter.ListType; -import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.traits.ExamplesTrait; @@ -160,36 +165,80 @@ private void writeExamples( String operationLinkId ) { writer.pushState(new ExamplesSection(context, operation, examples)); - if (examples.isEmpty()) { + var snippets = getExampleSnippets(context, operation, examples); + if (snippets.isEmpty()) { writer.popState(); return; } writer.openHeading("Examples", operationLinkId + "-examples"); for (var example : examples) { + var exampleSnippets = snippets.get(example.getTitle()); + if (exampleSnippets == null) { + continue; + } + writer.pushState(new ExampleSection(context, operation, example)); var linkIdSuffix = example.getTitle().toLowerCase(Locale.ENGLISH).strip().replaceAll("\\s+", "-"); writer.openHeading(example.getTitle(), operationLinkId + "-" + linkIdSuffix); example.getDocumentation().ifPresent(writer::writeCommonMark); writer.openTabGroup(); - // TODO: create example writer interface allow integrations to register them - // This is just a dummy placehodler tab here to exercise tab creation before - // there's an interface for it. - writer.openCodeTab("Input", "json"); - writer.write(Node.prettyPrintJson(example.getInput())); - writer.closeCodeTab(); - writer.openCodeTab("Output", "json"); - writer.write(Node.prettyPrintJson(example.getOutput().orElse(Node.objectNode()))); - writer.closeCodeTab(); + for (Snippet snippet : exampleSnippets) { + if (snippet.getFiles().size() == 1) { + var file = snippet.getFiles().get(0); + writer.openCodeTab(snippet.getTitle(), file.getLanguage()); + writer.write(file.getContent()); + writer.closeCodeTab(); + } else { + writer.openTab(snippet.getTitle()); + writer.openTabGroup(); + for (SnippetFile file : snippet.getFiles()) { + writer.openCodeTab(file.getFilename(), file.getLanguage()); + writer.write(file.getContent()); + writer.closeCodeTab(); + } + writer.closeTabGroup(); + writer.closeCodeTab(); + } + } writer.closeTabGroup(); - writer.closeHeading(); writer.popState(); } writer.closeHeading(); writer.popState(); } + + private Map> getExampleSnippets( + DocGenerationContext context, + OperationShape operation, + List examples + ) { + var exampleMap = examples.stream().collect(Collectors.groupingBy(Example::getTitle)); + var snippets = context.snippetConfig().getShapeSnippets(context.settings().service(), operation.getId()); + var resolvedSnippets = new LinkedHashMap>(); + for (Snippet snippet : snippets) { + + // Ensure that the service has the protocol if one is expected for the snippet. + // This could be a bad snippet, or it could be that the protocol was filtered + // out. + if (snippet.getProtocol().isPresent()) { + var service = context.model().expectShape(context.settings().service()); + if (!service.hasTrait(snippet.getProtocol().get())) { + continue; + } + } + + // Ensure that the snippet represent an example that's actually in the model. + var example = exampleMap.get(snippet.getTargetId()); + if (example == null) { + continue; + } + resolvedSnippets.computeIfAbsent(snippet.getTargetId(), id -> new ArrayList<>()).add(snippet); + } + return resolvedSnippets; + } } diff --git a/smithy-docgen/src/test/java/software/amazon/smithy/docgen/SmithyDocPluginTest.java b/smithy-docgen/src/test/java/software/amazon/smithy/docgen/SmithyDocPluginTest.java index 526102055dc..e82585f6ca2 100644 --- a/smithy-docgen/src/test/java/software/amazon/smithy/docgen/SmithyDocPluginTest.java +++ b/smithy-docgen/src/test/java/software/amazon/smithy/docgen/SmithyDocPluginTest.java @@ -5,46 +5,39 @@ package software.amazon.smithy.docgen; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Paths; +import java.util.Objects; import org.junit.jupiter.api.Test; import software.amazon.smithy.build.MockManifest; -import software.amazon.smithy.build.PluginContext; -import software.amazon.smithy.build.SmithyBuildPlugin; -import software.amazon.smithy.model.Model; -import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.docgen.utils.AbstractDocGenFileTest; +import software.amazon.smithy.model.node.ObjectNode; import software.amazon.smithy.utils.IoUtils; -public class SmithyDocPluginTest { +public class SmithyDocPluginTest extends AbstractDocGenFileTest { + private static final URL TEST_FILE = + Objects.requireNonNull(SmithyDocPluginTest.class.getResource("sample-service.smithy")); - @Test - public void assertDocumentationFiles() { - MockManifest manifest = new MockManifest(); - Model model = Model.assembler() - .addImport(getClass().getResource("sample-service.smithy")) - .discoverModels(getClass().getClassLoader()) - .assemble() - .unwrap(); - PluginContext context = PluginContext.builder() - .fileManifest(manifest) - .model(model) - .settings(Node.objectNodeBuilder() - .withMember("service", "smithy.example#SampleService") - .build()) - .build(); - - SmithyBuildPlugin plugin = new SmithyDocPlugin(); - plugin.execute(context); + @Override + protected URL testFile() { + return TEST_FILE; + } - assertFalse(manifest.getFiles().isEmpty()); - assertServicePageContents(manifest); + @Override + protected ObjectNode settings() { + return super.settings().toBuilder() + .withMember("service", "smithy.example#SampleService") + .build(); } - private void assertServicePageContents(MockManifest manifest) { - var actual = manifest.expectFileString("/content/index.md"); + @Test + public void assertDocumentationFiles() { + var fileManifest = new MockManifest(); + execute(fileManifest); + var actual = fileManifest.expectFileString("/content/index.md"); var expected = readExpectedPageContent("expected-outputs/index.md"); assertEquals(expected, actual); diff --git a/smithy-docgen/src/test/java/software/amazon/smithy/docgen/generators/OperationGeneratorTest.java b/smithy-docgen/src/test/java/software/amazon/smithy/docgen/generators/OperationGeneratorTest.java new file mode 100644 index 00000000000..96cc551f99b --- /dev/null +++ b/smithy-docgen/src/test/java/software/amazon/smithy/docgen/generators/OperationGeneratorTest.java @@ -0,0 +1,138 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package software.amazon.smithy.docgen.generators; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +import java.net.URL; +import java.nio.file.Path; +import java.util.Objects; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.docgen.utils.AbstractDocGenFileTest; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.utils.IoUtils; + +public class OperationGeneratorTest extends AbstractDocGenFileTest { + private static final URL TEST_FILE = + Objects.requireNonNull(OperationGeneratorTest.class.getResource("operation-generator.smithy")); + private static final URL SNIPPETS_FILE = + Objects.requireNonNull(OperationGeneratorTest.class.getResource("snippets.json")); + + @Override + protected URL testFile() { + return TEST_FILE; + } + + @Override + protected ObjectNode settings() { + return super.settings().toBuilder() + .withMember("snippetConfigs", Node.fromStrings(SNIPPETS_FILE.getFile())) + .build(); + } + + @Test + public void testOperationWithoutSnippetsShowsNoExamples() { + MockManifest manifest = new MockManifest(); + execute(manifest); + var operationDocs = manifest.expectFileString("/content/operations/NoSnippets.md"); + assertThat(operationDocs, not(containsString("Examples"))); + } + + @Test + public void testGeneratesSnippetsFromExplicitConfig() { + MockManifest manifest = new MockManifest(); + execute(manifest); + var operationDocs = manifest.expectFileString("/content/operations/BasicOperation.md"); + assertThat(operationDocs, containsString(""" + (basicoperation-examples)= + ## Examples + + (basicoperation-basic-example)= + ### Basic Example + + :::{tab} Text + :new-set: + + ```txt + Example pulled from explicit config. + ``` + ::: + :::{tab} Text 2 + ```txt + Example pulled from explicit config. + ``` + ::: + + (basicoperation-error-example)= + ### Error Example + + :::{tab} Text + :new-set: + + ```txt + Example pulled from explicit config. + ``` + ::: + :::{tab} Text 2 + ```txt + Example pulled from explicit config. + ``` + :::""")); + } + + @Test + public void testGeneratesSnippetsFromDiscoveredConfig(@TempDir Path tempDir) { + MockManifest manifest = new MockManifest(); + FileManifest sharedManifest = FileManifest.create(tempDir); + ObjectNode settings = settings().toBuilder() + .withoutMember("snippetConfigs") + .build(); + + sharedManifest.writeFile("snippets/snippets.json", IoUtils.readUtf8File(SNIPPETS_FILE.getFile())); + execute(manifest, sharedManifest, settings); + var operationDocs = manifest.expectFileString("/content/operations/BasicOperation.md"); + assertThat(operationDocs, containsString(""" + (basicoperation-examples)= + ## Examples + + (basicoperation-basic-example)= + ### Basic Example + + :::{tab} Text + :new-set: + + ```txt + Example pulled from explicit config. + ``` + ::: + :::{tab} Text 2 + ```txt + Example pulled from explicit config. + ``` + ::: + + (basicoperation-error-example)= + ### Error Example + + :::{tab} Text + :new-set: + + ```txt + Example pulled from explicit config. + ``` + ::: + :::{tab} Text 2 + ```txt + Example pulled from explicit config. + ``` + :::""")); + } +} diff --git a/smithy-docgen/src/test/java/software/amazon/smithy/docgen/utils/AbstractDocGenFileTest.java b/smithy-docgen/src/test/java/software/amazon/smithy/docgen/utils/AbstractDocGenFileTest.java new file mode 100644 index 00000000000..c169ebb0feb --- /dev/null +++ b/smithy-docgen/src/test/java/software/amazon/smithy/docgen/utils/AbstractDocGenFileTest.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.docgen.utils; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.net.URL; +import org.junit.jupiter.api.BeforeEach; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.docgen.SmithyDocPlugin; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; + +public abstract class AbstractDocGenFileTest { + + protected Model model; + protected final SmithyDocPlugin plugin = new SmithyDocPlugin(); + + @BeforeEach + public void setup() { + model = Model.assembler() + .addImport(testFile()) + .discoverModels(getClass().getClassLoader()) + .assemble() + .unwrap(); + } + + public void execute(FileManifest fileManifest) { + execute(fileManifest, new MockManifest(), settings()); + } + + public void execute(FileManifest fileManifest, FileManifest sharedFileManifest, ObjectNode settings) { + PluginContext context = PluginContext.builder() + .fileManifest(fileManifest) + .sharedFileManifest(sharedFileManifest) + .model(model) + .settings(settings) + .build(); + plugin.execute(context); + assertFalse(fileManifest.getFiles().isEmpty()); + } + + protected abstract URL testFile(); + + protected ObjectNode settings() { + return Node.objectNodeBuilder() + .withMember("service", "smithy.example#TestService") + .withMember("format", "sphinx-markdown") + .withMember("integrations", + Node.objectNodeBuilder() + .withMember("sphinx", + Node.objectNodeBuilder() + .withMember("autoBuild", false) + .build()) + .build()) + .build(); + } +} diff --git a/smithy-docgen/src/test/resources/software/amazon/smithy/docgen/generators/operation-generator.smithy b/smithy-docgen/src/test/resources/software/amazon/smithy/docgen/generators/operation-generator.smithy new file mode 100644 index 00000000000..65c246279cd --- /dev/null +++ b/smithy-docgen/src/test/resources/software/amazon/smithy/docgen/generators/operation-generator.smithy @@ -0,0 +1,85 @@ +$version: "2" + +namespace smithy.example + +service TestService { + operations: [ + NoSnippets + BasicOperation + ] +} + +@examples([ + { + title: "Basic Example" + input: { + foo: "foo" + } + output: { + bar: "bar" + } + } + { + title: "Error Example" + input: { + foo: "bar" + } + error: { + shapeId: BasicError + content: { + message: "bar" + } + } + } +]) +operation BasicOperation { + input := { + foo: String + } + output := { + bar: String + } + errors: [ + BasicError + ] +} + +@examples([ + { + title: "Basic Example" + input: { + foo: "foo" + } + output: { + bar: "bar" + } + } + { + title: "Error Example" + input: { + foo: "bar" + } + error: { + shapeId: BasicError + content: { + message: "bar" + } + } + } +]) +operation NoSnippets { + input := { + foo: String + } + output := { + bar: String + } + errors: [ + BasicError + ] +} + +@error("client") +structure BasicError { + message: String +} diff --git a/smithy-docgen/src/test/resources/software/amazon/smithy/docgen/generators/snippets.json b/smithy-docgen/src/test/resources/software/amazon/smithy/docgen/generators/snippets.json new file mode 100644 index 00000000000..fd7f79ce2e7 --- /dev/null +++ b/smithy-docgen/src/test/resources/software/amazon/smithy/docgen/generators/snippets.json @@ -0,0 +1,53 @@ +{ + "version": "1.0", + "snippets": { + "smithy.example#TestService": { + "smithy.example#BasicOperation": [ + { + "targetId": "Basic Example", + "title": "Text", + "files": [ + { + "language": "txt", + "filename": "example.txt", + "content": "Example pulled from explicit config." + } + ] + }, + { + "targetId": "Basic Example", + "title": "Text 2", + "files": [ + { + "language": "txt", + "filename": "example.txt", + "content": "Example pulled from explicit config." + } + ] + }, + { + "targetId": "Error Example", + "title": "Text", + "files": [ + { + "language": "txt", + "filename": "example.txt", + "content": "Example pulled from explicit config." + } + ] + }, + { + "targetId": "Error Example", + "title": "Text 2", + "files": [ + { + "language": "txt", + "filename": "example.txt", + "content": "Example pulled from explicit config." + } + ] + } + ] + } + } +} From dfc75de5e39b7293b76d2bf19ef8b4e569e24b73 Mon Sep 17 00:00:00 2001 From: Jordon Phillips Date: Tue, 16 Dec 2025 18:42:32 +0100 Subject: [PATCH 4/6] Add pr lins to staged changelogs Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../feature-3c70351cce599e85b810d4418fee8e5ca661436e.json | 4 +++- .../feature-df6f8fb30971b29b76010e7143df11affcb9618f.json | 4 +++- .../feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.changes/next-release/feature-3c70351cce599e85b810d4418fee8e5ca661436e.json b/.changes/next-release/feature-3c70351cce599e85b810d4418fee8e5ca661436e.json index c375cd3ee6c..6ee989999f4 100644 --- a/.changes/next-release/feature-3c70351cce599e85b810d4418fee8e5ca661436e.json +++ b/.changes/next-release/feature-3c70351cce599e85b810d4418fee8e5ca661436e.json @@ -1,5 +1,7 @@ { "type": "feature", "description": "Updated smithy-docgen to consume snippet files to generate example sections.", - "pull_requests": [] + "pull_requests": [ + "[#2894](https://github.com/smithy-lang/smithy/pull/2894)" + ] } diff --git a/.changes/next-release/feature-df6f8fb30971b29b76010e7143df11affcb9618f.json b/.changes/next-release/feature-df6f8fb30971b29b76010e7143df11affcb9618f.json index f429655702c..0f5c9ae8773 100644 --- a/.changes/next-release/feature-df6f8fb30971b29b76010e7143df11affcb9618f.json +++ b/.changes/next-release/feature-df6f8fb30971b29b76010e7143df11affcb9618f.json @@ -1,5 +1,7 @@ { "type": "feature", "description": "Added a `SnippetConfig` class to codgen-core that allows sharing doc snippets that will be consumed by smithy-docgen.", - "pull_requests": [] + "pull_requests": [ + "[#2894](https://github.com/smithy-lang/smithy/pull/2894)" + ] } diff --git a/.changes/next-release/feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json b/.changes/next-release/feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json index 698169e39f6..193a8451956 100644 --- a/.changes/next-release/feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json +++ b/.changes/next-release/feature-e6e564588167f70492cf365c0663ba0b17f0a3d8.json @@ -2,6 +2,6 @@ "type": "feature", "description": "Made `DefaultBuilderRef` public.", "pull_requests": [ - "" + "[#2894](https://github.com/smithy-lang/smithy/pull/2894)" ] } From b6286a5bb13c177c2b86508004a913600d856dde Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 19 Dec 2025 10:01:47 +0100 Subject: [PATCH 5/6] Share snippet map copying code This updates the snippet map copying functions to share implementation details. --- .../codegen/core/docs/SnippetConfig.java | 25 ++++++++++--------- .../codegen/core/docs/SnippetConfigTest.java | 10 +++++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetConfig.java b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetConfig.java index 9a3eca82035..b68a9c0e112 100644 --- a/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetConfig.java +++ b/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/docs/SnippetConfig.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Function; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.node.NodeMapper; import software.amazon.smithy.model.node.ObjectNode; @@ -154,29 +155,29 @@ public final static class Builder implements SmithyBuilder { private static Map>> copySnippetMap( Map>> snippets ) { - LinkedHashMap>> copy = new LinkedHashMap<>(); - for (Map.Entry>> entry : snippets.entrySet()) { - LinkedHashMap> serviceCopy = new LinkedHashMap<>(); - for (Map.Entry> serviceEntry : entry.getValue().entrySet()) { - serviceCopy.put(serviceEntry.getKey(), new ArrayList<>(serviceEntry.getValue())); - } - copy.put(entry.getKey(), serviceCopy); - } - return copy; + return copySnippetMap(snippets, ArrayList::new, Function.identity()); } private static Map>> immutableWrapSnippetMap( Map>> snippets + ) { + return MapUtils.orderedCopyOf(copySnippetMap(snippets, ListUtils::copyOf, MapUtils::orderedCopyOf)); + } + + private static Map>> copySnippetMap( + Map>> snippets, + Function, List> copySnippetList, + Function>, Map>> finalizeServiceCopy ) { LinkedHashMap>> copy = new LinkedHashMap<>(); for (Map.Entry>> entry : snippets.entrySet()) { LinkedHashMap> serviceCopy = new LinkedHashMap<>(); for (Map.Entry> serviceEntry : entry.getValue().entrySet()) { - serviceCopy.put(serviceEntry.getKey(), ListUtils.copyOf(serviceEntry.getValue())); + serviceCopy.put(serviceEntry.getKey(), copySnippetList.apply(serviceEntry.getValue())); } - copy.put(entry.getKey(), MapUtils.orderedCopyOf(serviceCopy)); + copy.put(entry.getKey(), finalizeServiceCopy.apply(serviceCopy)); } - return MapUtils.orderedCopyOf(copy); + return copy; } @Override diff --git a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/docs/SnippetConfigTest.java b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/docs/SnippetConfigTest.java index 682b2526845..29e9dd3a857 100644 --- a/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/docs/SnippetConfigTest.java +++ b/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/docs/SnippetConfigTest.java @@ -6,12 +6,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.net.URL; import java.nio.file.Paths; import java.util.List; +import java.util.Objects; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import software.amazon.smithy.model.node.Node; import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.IoUtils; import software.amazon.smithy.utils.ListUtils; import software.amazon.smithy.utils.MapUtils; @@ -118,19 +121,20 @@ public static void setup() { @Test public void serializesToNode() throws Exception { Node result = snippetConfig.toNode(); - Node expected = Node.parse(getClass().getResource("snippet-config.json").openStream()); + Node expected = Node.parse(IoUtils.readUtf8Url(getClass().getResource("snippet-config.json"))); assertEquals(result, expected); } @Test public void parsesNode() throws Exception { - SnippetConfig actual = SnippetConfig.load(Paths.get(getClass().getResource("snippet-config.json").toURI())); + URL url = Objects.requireNonNull(getClass().getResource("snippet-config.json")); + SnippetConfig actual = SnippetConfig.load(Paths.get(url.toURI())); assertEquals(actual, snippetConfig); } @Test public void roundTrips() throws Exception { - Node expected = Node.parse(getClass().getResource("snippet-config.json").openStream()); + Node expected = Node.parse(IoUtils.readUtf8Url(getClass().getResource("snippet-config.json"))); Node actual = SnippetConfig.fromNode(expected).toNode(); Node.assertEquals(actual, expected); } From 3ccf75338160951d7a2ccb0c75937b7def4fc221 Mon Sep 17 00:00:00 2001 From: JordonPhillips Date: Fri, 19 Dec 2025 10:26:02 +0100 Subject: [PATCH 6/6] Fix windows path issues --- .../software/amazon/smithy/docgen/PluginTest.java | 9 +++++---- .../software/amazon/smithy/docgen/DocSettings.java | 3 +-- .../docgen/generators/OperationGeneratorTest.java | 11 +++++++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/smithy-docgen/src/it/java/software/amazon/smithy/docgen/PluginTest.java b/smithy-docgen/src/it/java/software/amazon/smithy/docgen/PluginTest.java index 79dbf9b4efd..8b3e85da3ef 100644 --- a/smithy-docgen/src/it/java/software/amazon/smithy/docgen/PluginTest.java +++ b/smithy-docgen/src/it/java/software/amazon/smithy/docgen/PluginTest.java @@ -9,6 +9,7 @@ import java.net.URL; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -40,7 +41,7 @@ public static void setup(@TempDir Path tempDir) { } @Test - public void pluginGeneratesMarkdown() { + public void pluginGeneratesMarkdown() throws Exception { ObjectNode settings = Node.objectNodeBuilder() .withMember("service", "com.example#DocumentedService") .withMember("format", "markdown") @@ -49,7 +50,7 @@ public void pluginGeneratesMarkdown() { Node.objectNodeBuilder() .withMember("com.example#ExternalResource", "https://aws.amazon.com") .build()) - .withMember("snippetConfigs", Node.fromStrings(SNIPPETS.getFile())) + .withMember("snippetConfigs", Node.fromStrings(Paths.get(SNIPPETS.toURI()).toString())) .build(); PluginContext context = getPluginContext(model, settings); @@ -64,7 +65,7 @@ public void pluginGeneratesMarkdown() { } @Test - public void pluginGeneratesSphinxMarkdown(@TempDir Path tempDir) { + public void pluginGeneratesSphinxMarkdown() throws Exception { Model model = getModel("main.smithy"); ObjectNode settings = Node.objectNodeBuilder() .withMember("service", "com.example#DocumentedService") @@ -74,7 +75,7 @@ public void pluginGeneratesSphinxMarkdown(@TempDir Path tempDir) { Node.objectNodeBuilder() .withMember("com.example#ExternalResource", "https://aws.amazon.com") .build()) - .withMember("snippetConfigs", Node.fromStrings(SNIPPETS.getFile())) + .withMember("snippetConfigs", Node.fromStrings(Paths.get(SNIPPETS.toURI()).toString())) .build(); PluginContext context = getPluginContext(model, settings); diff --git a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocSettings.java b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocSettings.java index 97e41d9e567..e0f2f6d98bf 100644 --- a/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocSettings.java +++ b/smithy-docgen/src/main/java/software/amazon/smithy/docgen/DocSettings.java @@ -5,7 +5,6 @@ package software.amazon.smithy.docgen; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.Objects; @@ -63,7 +62,7 @@ public static DocSettings fromNode(ObjectNode pluginSettings) { e -> e.getValue().expectStringNode().getValue())); var snippetConfigs = pluginSettings.getArrayMember("snippetConfigs") .orElse(Node.arrayNode()) - .getElementsAs(e -> Paths.get(e.expectStringNode().getValue())); + .getElementsAs(e -> Path.of(e.expectStringNode().getValue())); return new DocSettings( pluginSettings.expectStringMember("service").expectShapeId(), pluginSettings.getStringMemberOrDefault("format", "sphinx-markdown"), diff --git a/smithy-docgen/src/test/java/software/amazon/smithy/docgen/generators/OperationGeneratorTest.java b/smithy-docgen/src/test/java/software/amazon/smithy/docgen/generators/OperationGeneratorTest.java index 96cc551f99b..351955b359f 100644 --- a/smithy-docgen/src/test/java/software/amazon/smithy/docgen/generators/OperationGeneratorTest.java +++ b/smithy-docgen/src/test/java/software/amazon/smithy/docgen/generators/OperationGeneratorTest.java @@ -10,6 +10,7 @@ import java.net.URL; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Objects; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -33,8 +34,14 @@ protected URL testFile() { @Override protected ObjectNode settings() { + String path; + try { + path = Paths.get(SNIPPETS_FILE.toURI()).toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } return super.settings().toBuilder() - .withMember("snippetConfigs", Node.fromStrings(SNIPPETS_FILE.getFile())) + .withMember("snippetConfigs", Node.fromStrings(path)) .build(); } @@ -96,7 +103,7 @@ public void testGeneratesSnippetsFromDiscoveredConfig(@TempDir Path tempDir) { .withoutMember("snippetConfigs") .build(); - sharedManifest.writeFile("snippets/snippets.json", IoUtils.readUtf8File(SNIPPETS_FILE.getFile())); + sharedManifest.writeFile("snippets/snippets.json", IoUtils.readUtf8Url(SNIPPETS_FILE)); execute(manifest, sharedManifest, settings); var operationDocs = manifest.expectFileString("/content/operations/BasicOperation.md"); assertThat(operationDocs, containsString("""