From 2dd1d9dc7ec284c8e6f992be8267b88f386aed07 Mon Sep 17 00:00:00 2001 From: Jordon Phillips Date: Mon, 6 Nov 2023 15:16:07 +0100 Subject: [PATCH] WIP: snippet generator interface This adds and makes use of a snippet generator interface that lets other generators provide examples to be included in the docs. It's unfortunately more ugly than I'd like, notbably with config. Generators will essentially have to run much of their generator to be able to provide an accurate example, so we have to give a fake plugin context that they can use. The codegen director will need to be updated to make this more workable. This is just a proof of concept meant to show how it might work from the doc generator side. --- .../smithy/docgen/core/DirectedDocGen.java | 36 +++++- .../docgen/core/DocGenerationContext.java | 14 ++- .../smithy/docgen/core/DocSettings.java | 10 +- .../core/MockPluginContextGenerator.java | 73 +++++++++++++ .../smithy/docgen/core/SmithyDocPlugin.java | 2 +- .../generators/ExampleInputGenerator.java | 43 ++++++++ .../generators/ExampleOutputGenerator.java | 38 +++++++ .../core/generators/OperationGenerator.java | 17 +-- .../core/generators/SnippetGenerator.java | 103 ++++++++++++++++++ ...hy.docgen.core.generators.SnippetGenerator | 7 ++ 10 files changed, 322 insertions(+), 21 deletions(-) create mode 100644 smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/MockPluginContextGenerator.java create mode 100644 smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ExampleInputGenerator.java create mode 100644 smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ExampleOutputGenerator.java create mode 100644 smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/SnippetGenerator.java create mode 100644 smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.docgen.core.generators.SnippetGenerator diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DirectedDocGen.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DirectedDocGen.java index 20f75d9..6fc98dc 100644 --- a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DirectedDocGen.java +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DirectedDocGen.java @@ -5,6 +5,10 @@ package software.amazon.smithy.docgen.core; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import software.amazon.smithy.build.PluginContext; import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.directed.CreateContextDirective; import software.amazon.smithy.codegen.core.directed.CreateSymbolProviderDirective; @@ -21,6 +25,8 @@ import software.amazon.smithy.docgen.core.generators.OperationGenerator; import software.amazon.smithy.docgen.core.generators.ResourceGenerator; import software.amazon.smithy.docgen.core.generators.ServiceGenerator; +import software.amazon.smithy.docgen.core.generators.SnippetGenerator; +import software.amazon.smithy.docgen.core.generators.SnippetGenerator.SnippetComparator; import software.amazon.smithy.docgen.core.generators.StructuredShapeGenerator; import software.amazon.smithy.model.node.ExpectationNotMetException; import software.amazon.smithy.model.traits.InputTrait; @@ -33,6 +39,12 @@ @SmithyUnstableApi final class DirectedDocGen implements DirectedCodegen { + private final PluginContext pluginContext; + + DirectedDocGen(PluginContext pluginContext) { + this.pluginContext = pluginContext; + } + @Override public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective directive) { return new DocSymbolProvider(directive.model(), directive.settings()); @@ -40,12 +52,26 @@ public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective directive) { + var contextGenerator = new MockPluginContextGenerator( + pluginContext, directive.settings().snippetGeneratorSettings()); + var classLoader = pluginContext.getPluginClassLoader().orElse(getClass().getClassLoader()); + + List snippetGenerators = new ArrayList<>(); + for (SnippetGenerator generator : ServiceLoader.load(SnippetGenerator.class, classLoader)) { + var pluginContext = contextGenerator.getStubbedContext(directive.model(), generator.name()); + if (generator.configure(pluginContext, directive.service())) { + snippetGenerators.add(generator); + } + } + snippetGenerators = snippetGenerators.stream().sorted(new SnippetComparator()).toList(); + return new DocGenerationContext( - directive.model(), - directive.settings(), - directive.symbolProvider(), - directive.fileManifest(), - directive.integrations() + directive.model(), + directive.settings(), + directive.symbolProvider(), + directive.fileManifest(), + directive.integrations(), + snippetGenerators ); } diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocGenerationContext.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocGenerationContext.java index 0d411d2..3c17aa6 100644 --- a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocGenerationContext.java +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocGenerationContext.java @@ -13,6 +13,7 @@ import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.codegen.core.WriterDelegator; import software.amazon.smithy.docgen.core.DocSymbolProvider.FileExtensionDecorator; +import software.amazon.smithy.docgen.core.generators.SnippetGenerator; import software.amazon.smithy.docgen.core.writers.DocWriter; import software.amazon.smithy.model.Model; import software.amazon.smithy.utils.SmithyUnstableApi; @@ -30,6 +31,7 @@ public final class DocGenerationContext implements CodegenContext writerDelegator; private final List docIntegrations; private final DocFormat docFormat; + private final List snippetGenerators; /** * Constructor. @@ -39,13 +41,15 @@ public final class DocGenerationContext implements CodegenContext docIntegrations + List docIntegrations, + List snippetGenerators ) { this.model = model; this.docSettings = docSettings; @@ -74,6 +78,7 @@ public DocGenerationContext( this.docFormat = resolvedFormat; this.symbolProvider = symbolProvider; this.writerDelegator = new WriterDelegator<>(fileManifest, symbolProvider, resolvedFormat.writerFactory()); + this.snippetGenerators = snippetGenerators; } @Override @@ -112,4 +117,11 @@ public List integrations() { public DocFormat docFormat() { return this.docFormat; } + + /** + * @return returns the generators used to add snippets to docs. + */ + public List snippetGenerators() { + return snippetGenerators; + } } diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSettings.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSettings.java index 257c59f..69e7f19 100644 --- a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSettings.java +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSettings.java @@ -6,6 +6,7 @@ package software.amazon.smithy.docgen.core; import java.util.Objects; +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; @@ -16,9 +17,12 @@ * * @param service The shape id of the service to generate documentation for. * @param format The format to generate documentation in. The default is markdown. + * @param snippetGeneratorSettings Settings to pass along to snippet generators. By + * default, the settings for the plugin in the current projection will be used, + * if available. */ @SmithyUnstableApi -public record DocSettings(ShapeId service, String format) { +public record DocSettings(ShapeId service, String format, ObjectNode snippetGeneratorSettings) { /** * Settings for documentation generation. These can be set in the @@ -30,6 +34,7 @@ public record DocSettings(ShapeId service, String format) { public DocSettings { Objects.requireNonNull(service); Objects.requireNonNull(format); + Objects.requireNonNull(snippetGeneratorSettings); } /** @@ -41,7 +46,8 @@ public record DocSettings(ShapeId service, String format) { public static DocSettings fromNode(ObjectNode pluginSettings) { return new DocSettings( pluginSettings.expectStringMember("service").expectShapeId(), - pluginSettings.getStringMemberOrDefault("format", "sphinx-markdown") + pluginSettings.getStringMemberOrDefault("format", "sphinx-markdown"), + pluginSettings.getObjectMember("snippetGeneratorSettings").orElse(Node.objectNode()) ); } } diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/MockPluginContextGenerator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/MockPluginContextGenerator.java new file mode 100644 index 0000000..de68a17 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/MockPluginContextGenerator.java @@ -0,0 +1,73 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core; + +import java.util.Optional; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.model.ProjectionConfig; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates mock plugin contexts to pass along to snippet generators. + */ +@SmithyUnstableApi +class MockPluginContextGenerator { + private final PluginContext baseContext; + private final ObjectNode pluginSettings; + + /** + * Constructs a MockPluginContextGenerator. + * + * @param baseContext The context to base plugin context generation on. + * @param pluginSettings Settings specifically given to pass to plugins via the doc generator. + */ + MockPluginContextGenerator(PluginContext baseContext, ObjectNode pluginSettings) { + if (baseContext.getProjection().isEmpty()) { + // PluginContext will NPE if there's no projection config so you have to do it manually + // until that gets fixed. + var builder = PluginContext.builder() + .projection(baseContext.getProjectionName(), ProjectionConfig.builder().build()) + .model(baseContext.getModel()) + .originalModel(baseContext.getOriginalModel().orElse(baseContext.getModel())) + .events(baseContext.getEvents()) + .settings(baseContext.getSettings()) + .fileManifest(baseContext.getFileManifest()) + .sources(baseContext.getSources()); + baseContext.getPluginClassLoader().ifPresent(builder::pluginClassLoader); + baseContext.getOriginalModel().ifPresent(builder::originalModel); + baseContext.getArtifactName().ifPresent(builder::artifactName); + baseContext = builder.build(); + } + this.baseContext = baseContext; + this.pluginSettings = pluginSettings; + } + + /** + * Create a new plugin context with the given model and plugin name. + * + * @param model The version of the model to hand to the plugin. + * @param pluginName The name of the plugin. This is used to search for existing config. + * @return Returns a plugin context with the given model and config for the named plugin. + */ + PluginContext getStubbedContext(Model model, String pluginName) { + ObjectNode settings = pluginSettings.getObjectMember(pluginName) + .or(() -> baseContext.getProjection() + .map(ProjectionConfig::getPlugins) + .flatMap(plugins -> Optional.ofNullable(plugins.get(pluginName)))) + .orElse(Node.objectNode()); + + return baseContext.toBuilder() + .fileManifest(new MockManifest()) + .artifactName(pluginName) + .model(model) + .settings(settings) + .build(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/SmithyDocPlugin.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/SmithyDocPlugin.java index e3c14d6..3410e98 100644 --- a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/SmithyDocPlugin.java +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/SmithyDocPlugin.java @@ -42,7 +42,7 @@ public void execute(PluginContext pluginContext) { CodegenDirector runner = new CodegenDirector<>(); - runner.directedCodegen(new DirectedDocGen()); + runner.directedCodegen(new DirectedDocGen(pluginContext)); runner.integrationClass(DocIntegration.class); runner.fileManifest(pluginContext.getFileManifest()); runner.model(getValidatedModel(pluginContext.getModel()).unwrap()); diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ExampleInputGenerator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ExampleInputGenerator.java new file mode 100644 index 0000000..ccbcf7d --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ExampleInputGenerator.java @@ -0,0 +1,43 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.generators; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.ExamplesTrait.Example; + +public class ExampleInputGenerator implements SnippetGenerator { + @Override + public String name() { + return "input"; + } + + @Override + public boolean isWireProtocolGenerator() { + return true; + } + + @Override + public String tabTitle() { + return "Input"; + } + + @Override + public String language() { + return "json"; + } + + @Override + public String generateShapeSnippet(Shape shape, Node value) { + return ""; + } + + @Override + public String generateExampleSnippet(OperationShape operation, Example example) { + return Node.prettyPrintJson(example.getInput()); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ExampleOutputGenerator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ExampleOutputGenerator.java new file mode 100644 index 0000000..f2aff4e --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ExampleOutputGenerator.java @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.generators; + +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.ExamplesTrait.Example; + +public class ExampleOutputGenerator implements SnippetGenerator { + @Override + public String name() { + return "output"; + } + + @Override + public String tabTitle() { + return "Output"; + } + + @Override + public String language() { + return "json"; + } + + @Override + public String generateShapeSnippet(Shape shape, Node value) { + return ""; + } + + @Override + public String generateExampleSnippet(OperationShape operation, Example example) { + return Node.prettyPrintJson(example.getOutput().orElse(Node.objectNode())); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/OperationGenerator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/OperationGenerator.java index d616f14..f046472 100644 --- a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/OperationGenerator.java +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/OperationGenerator.java @@ -21,7 +21,6 @@ import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; import software.amazon.smithy.docgen.core.writers.DocWriter; import software.amazon.smithy.docgen.core.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; @@ -159,17 +158,11 @@ private void writeExamples( 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 (var snippetGenerator : context.snippetGenerators()) { + writer.openCodeTab(snippetGenerator.tabTitle(), snippetGenerator.language()); + writer.write(snippetGenerator.generateExampleSnippet(operation, example)); + writer.closeCodeTab(); + } writer.closeTabGroup(); writer.closeHeading(); diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/SnippetGenerator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/SnippetGenerator.java new file mode 100644 index 0000000..906c1b8 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/SnippetGenerator.java @@ -0,0 +1,103 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.generators; + +import java.io.Serializable; +import java.util.Comparator; +import software.amazon.smithy.build.PluginContext; +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.shapes.Shape; +import software.amazon.smithy.model.traits.ExamplesTrait.Example; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates snippets for documentation. + */ +@SmithyUnstableApi +public interface SnippetGenerator { + + /** + * Gets the name to use for the generator. + * + *

If this snippet generator is associated to a code generator, this must match + * the name of that code generator plugin. + * + * @return the generator's name. + */ + default String name() { + return getClass().getCanonicalName(); + } + + /** + * + * @return returns whether the generator generates wire protocol snippets. + */ + default boolean isWireProtocolGenerator() { + return false; + } + + /** + * Configures the snippet generator. + * + *

If the generator can't be configured with the given configuration, or it + * doesn't support the provided service, it should return {@code false} + * + * @param context A synthetic plugin context to configure the generator with. + * @param service The service being generated for. This takes priority over any + * configuration present. + * @return returns whether the generator is configurable. + */ + default boolean configure(PluginContext context, ServiceShape service) { + return true; + } + + /** + * @return returns the title to use in code tabs. + */ + String tabTitle(); + + /** + * @return returns the language used to format code blocks. + */ + String language(); + + /** + * Generates a snippet that instantiates a shape. + * + * @param shape The shape to instantiate. + * @param value The value that the shape should be instantiated with. + * @return returns a string that demonstrates instantiating the shape. + */ + String generateShapeSnippet(Shape shape, Node value); + + /** + * Generates a snippet for a given modeled example. + * + * @param operation The operation whose example is being generated. + * @param example The example to generate. + * @return returns a string representing the example. + */ + String generateExampleSnippet(OperationShape operation, Example example); + + /** + * Sorts snippet generators with wire protocol generators in front, and + * then alphabetically by name. + */ + final class SnippetComparator implements Comparator, Serializable { + + @Override + public int compare(SnippetGenerator sg1, SnippetGenerator sg2) { + if (sg1.isWireProtocolGenerator() && !sg2.isWireProtocolGenerator()) { + return 1; + } else if (!sg1.isWireProtocolGenerator() && sg2.isWireProtocolGenerator()) { + return -1; + } + return String.CASE_INSENSITIVE_ORDER.compare(sg1.name(), sg2.name()); + } + } +} diff --git a/smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.docgen.core.generators.SnippetGenerator b/smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.docgen.core.generators.SnippetGenerator new file mode 100644 index 0000000..53bbefa --- /dev/null +++ b/smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.docgen.core.generators.SnippetGenerator @@ -0,0 +1,7 @@ +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# + +software.amazon.smithy.docgen.core.generators.ExampleInputGenerator +software.amazon.smithy.docgen.core.generators.ExampleOutputGenerator