diff --git a/build.gradle.kts b/build.gradle.kts index 5de3130ef5f..7c112d60543 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,12 +46,15 @@ val stagingDirectory = rootProject.layout.buildDirectory.dir("staging") subprojects { apply(plugin = "java-library") - java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + if (project.name == "smithy-docgen-core" || project.name == "smithy-docgen-test") { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } else { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } } - repositories { mavenLocal() mavenCentral() diff --git a/settings.gradle.kts b/settings.gradle.kts index e3703ba7352..3bc4359ec0b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -42,4 +42,6 @@ include(":smithy-aws-endpoints") include(":smithy-aws-smoke-test-model") include(":smithy-protocol-traits") include(":smithy-protocol-tests") -include(":smithy-trait-codegen") \ No newline at end of file +include(":smithy-trait-codegen") +include(":smithy-docgen-core") +include(":smithy-docgen-test") \ No newline at end of file diff --git a/smithy-docgen-core/README.md b/smithy-docgen-core/README.md new file mode 100644 index 00000000000..c3aa2f2c311 --- /dev/null +++ b/smithy-docgen-core/README.md @@ -0,0 +1,197 @@ +## Smithy DocGen + +Smithy build plugin to generate API documentation from models authored in +[Smithy](https://smithy.io) IDL. + +NOTE: this project is currently in a pre-release state. Interfaces and output +formatting may change before full release. + +### Usage + +First, create a gradle-based Smithy model project. This can be done easily with +the Smithy CLI: `smithy init -o /tmp/smithy-docgen -t quickstart-gradle`. + +Note: the generator currently cannot be run with non-gradle-based projects. + +Next, publish the generator to Maven local by running +`./gradlew :smithy-docgen-core:publishToMavenLocal`. + +In `build.gradle.kts`, add +`implementation("software.amazon.smithy:smithy-docgen-core:$smithyVersion")` under +`implementation`. + +Next, add the `docgen` plugin to the +[plugin configuration](https://smithy.io/2.0/guides/smithy-build-json.html) in +`smithy-build.json`: + +```json +"plugins": { + "docgen": { + "service": "example.weather#Weather" + } +} +``` + +Finally, build the model with Gradle: `./gradlew build` + +Build logs will provide the destination folder for the generated docs. + +### Current State + +A documentation site can be generated in one of two formats with wide support +for built-in traits. Minor changes to layout may occur, but the final product +will be similar to the current output. + +The one critical, missing component is full example support. This will drive both +wire-level examples and client examples. + +#### Trait Support + +Currently, most prelude (`smithy.api`) traits are supported, or deliberately +excluded where not relevant to customer documentation. Trait information is +easily added using Smithy's +[interceptor](https://github.com/smithy-lang/smithy/blob/main/smithy-utils/src/main/java/software/amazon/smithy/utils/CodeInterceptor.java) +system. Most trait information is added using interceptors, the implementations +of which can be found in the +[interceptors](https://github.com/smithy-lang/smithy-docgen/tree/main/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors) +package. + +Auth traits are automatically supported as part of the service's auth list, +where the trait's docs are used by default. More context can be added using +the same interceptors that are run on normal shapes. + +##### Traits Missing Support + +The following prelude traits and trait categories are currently unsupported. All +traits outside of the prelude are unsupported, with the exception of auth traits +which have default support. + +* Protocol Traits - These should get a similar treatment to auth traits, where a + dedicated section is created for them and documentation is added without + needing to add explicit support. Each protocol also needs to be able to register + an example generator. +* [Event Streaming](https://smithy.io/2.0/spec/streaming.html#event-streams) +* [examples](https://smithy.io/2.0/spec/documentation-traits.html#smithy-api-examples-trait) - + The sections and wrapping for these are created, and currently there's a + stub that simply places the values of example inputs and outputs inside the + example blocks. An interface needs to be created for code generators to + actually integrate into this. Updates to directed codegen will likely be + needed to make this feasible. Protocols will need to implement this also. + +### Configuration + +This generator supports the following top-level configuration options: + +* `service` - The shape ID of the service to generate documentation for. +* `format` - The format that the documentation should be generated in. +* `references` - A map of resource shape ID to URL for resources referenced by + the [references trait](https://smithy.io/2.0/spec/resource-traits.html#references-trait) + that aren't included in service. + +```json +{ + "version": "1.0", + "projections": { + "plain-markdown": { + "plugins": { + "docgen": { + "service": "com.example#MyService", + "format": "markdown", + "references": { + "com.example#ExternalReference": "https://example.com/" + } + } + } + } + } +} +``` + +#### Supported Formats + +The output format can be selected with the `format` configuration option. The +example below demonstrtates selecting a plain markdown output format: + +```json +{ + "version": "1.0", + "projections": { + "plain-markdown": { + "plugins": { + "docgen": { + "service": "com.example#MyService", + "format": "markdown" + } + } + } + } +} +``` + +By default, two formats are currently supported: `markdown` and +`sphinx-markdown`. The `markdown` format renders docs as plain +[CommonMark](https://commonmark.org), while `sphinx-commonmark` creates a +[Sphinx](https://www.sphinx-doc.org/) markdown project that gets rendered to +HTTP. `sphinx-markdown` is used by default. + +The generator is designed to allow for different output formats by supplying a +new +[DocWriter](https://github.com/smithy-lang/smithy-docgen/blob/main/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/DocWriter.java) +via a +[DocIntegration](https://github.com/smithy-lang/smithy-docgen/blob/main/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocIntegration.java). + +##### sphinx-markdown + +The `sphinx-markdown` format uses Sphinx's markdown support provided by +[MySt](https://myst-parser.readthedocs.io/en/latest/), which builds on top of +CommonMark. By default, it will render the generated markdown into HTML as long +as Python 3 is found on the path. + +* `format` (default: `html`) - The + [sphinx output format](https://www.sphinx-doc.org/en/master/usage/builders/index.html). +* `theme` (default: [`furo`](https://github.com/pradyunsg/furo)) - The theme to + use for sphinx. If this is changed, the new theme will likely need to be added + to the `extraDependencies` list. +* `extraDependencies` (default: `[]`) - A list of additional dependencies to be + added to the `requirements.txt` file, which is installed before building the + documentation. +* `extraExtensions` (default: `[]`) - A list of additional sphinx extentions to + add to the sphinx extensions list in `conf.py`. Any additional extensions will + likely need to be added to the `extraDependencies` list. +* `autoBuild` (default: `true`) - Whether to automatically render the + documentation to HTML. You may wish to disable autobuild if you want to add + additional documentation to the project before building, such as hand-written + guides. + +The following example `smithy-build.json` demonstrates configuring the +`sphinx-markdown` format. + +```json +{ + "version": "1.0", + "projections": { + "sphinx-markdown": { + "plugins": { + "docgen": { + "service": "com.example#DocumentedService", + "format": "sphinx-markdown", + "integrations": { + "sphinx": { + "format": "dirhtml", + "autoBuild": false + } + } + } + } + } + } +} +``` + +## Security + +See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. + +## License + +This project is licensed under the Apache-2.0 License. diff --git a/smithy-docgen-core/build.gradle.kts b/smithy-docgen-core/build.gradle.kts new file mode 100644 index 00000000000..884e9b4f456 --- /dev/null +++ b/smithy-docgen-core/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +description = "This module contains support for generating API documentation " + + "based on Smithy models." + + +extra["displayName"] = "Smithy :: DocGen :: Core" +extra["moduleName"] = "software.amazon.smithy.docgen.core" + +dependencies { + api(project(":smithy-model")) + api(project(":smithy-build")) + api(project(":smithy-utils")) + api(project(":smithy-codegen-core")) + api(project(":smithy-linters")) +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..20f75d9e874 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DirectedDocGen.java @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core; + +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.codegen.core.directed.CreateContextDirective; +import software.amazon.smithy.codegen.core.directed.CreateSymbolProviderDirective; +import software.amazon.smithy.codegen.core.directed.DirectedCodegen; +import software.amazon.smithy.codegen.core.directed.GenerateEnumDirective; +import software.amazon.smithy.codegen.core.directed.GenerateErrorDirective; +import software.amazon.smithy.codegen.core.directed.GenerateIntEnumDirective; +import software.amazon.smithy.codegen.core.directed.GenerateOperationDirective; +import software.amazon.smithy.codegen.core.directed.GenerateResourceDirective; +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.docgen.core.generators.MemberGenerator.MemberListingType; +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.StructuredShapeGenerator; +import software.amazon.smithy.model.node.ExpectationNotMetException; +import software.amazon.smithy.model.traits.InputTrait; +import software.amazon.smithy.model.traits.OutputTrait; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * The main entry points for documentation generation. + */ +@SmithyUnstableApi +final class DirectedDocGen implements DirectedCodegen { + + @Override + public SymbolProvider createSymbolProvider(CreateSymbolProviderDirective directive) { + return new DocSymbolProvider(directive.model(), directive.settings()); + } + + @Override + public DocGenerationContext createContext(CreateContextDirective directive) { + return new DocGenerationContext( + directive.model(), + directive.settings(), + directive.symbolProvider(), + directive.fileManifest(), + directive.integrations() + ); + } + + @Override + public void generateService(GenerateServiceDirective directive) { + new ServiceGenerator().accept(directive); + } + + @Override + public void generateStructure(GenerateStructureDirective directive) { + // Input and output structures are documented alongside the relevant operations. + if (directive.shape().hasTrait(InputTrait.class) || directive.shape().hasTrait(OutputTrait.class)) { + return; + } + new StructuredShapeGenerator(directive.context()).accept(directive.shape(), MemberListingType.MEMBERS); + } + + @Override + public void generateOperation(GenerateOperationDirective directive) { + new OperationGenerator().accept(directive); + } + + @Override + public void generateError(GenerateErrorDirective directive) { + new StructuredShapeGenerator(directive.context()).accept(directive.shape(), MemberListingType.MEMBERS); + } + + @Override + public void generateUnion(GenerateUnionDirective directive) { + new StructuredShapeGenerator(directive.context()).accept(directive.shape(), MemberListingType.OPTIONS); + } + + @Override + public void generateEnumShape(GenerateEnumDirective directive) { + new StructuredShapeGenerator(directive.context()).accept(directive.shape(), MemberListingType.OPTIONS); + } + + @Override + public void generateIntEnumShape(GenerateIntEnumDirective directive) { + var shape = directive.shape(); + var intEnum = shape.asIntEnumShape().orElseThrow(() -> new ExpectationNotMetException( + "Expected an intEnum shape, but found " + shape, shape)); + new StructuredShapeGenerator(directive.context()).accept(intEnum, MemberListingType.OPTIONS); + } + + @Override + public void generateResource(GenerateResourceDirective directive) { + new ResourceGenerator().accept(directive.context(), directive.shape()); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocFormat.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocFormat.java new file mode 100644 index 00000000000..466dc0211ff --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocFormat.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core; + +import software.amazon.smithy.codegen.core.SymbolWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A record containing information about a doc format. + * + *

Use {@link DocIntegration#docFormats} to make new formats available. + * + * @param name The name of the format. This will be the string that will be set as the + * value of {@code format} in {@link DocSettings}. + * @param extension The file extension to use by default for documentation files. This + * will be set on the {@code Symbol}s automatically by + * {@link software.amazon.smithy.docgen.core.DocSymbolProvider.FileExtensionDecorator}. + * @param writerFactory A factory method for creating writers that write in this + * format. + */ +@SmithyUnstableApi +public record DocFormat(String name, String extension, SymbolWriter.Factory writerFactory) { +} 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 new file mode 100644 index 00000000000..0d411d2c6a6 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocGenerationContext.java @@ -0,0 +1,115 @@ +/* + * 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.LinkedHashSet; +import java.util.List; +import software.amazon.smithy.build.FileManifest; +import software.amazon.smithy.codegen.core.CodegenContext; +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.docgen.core.DocSymbolProvider.FileExtensionDecorator; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contextual information that is made available during most parts of documentation + * generation. + */ +@SmithyUnstableApi +public final class DocGenerationContext implements CodegenContext { + private final Model model; + private final DocSettings docSettings; + private final SymbolProvider symbolProvider; + private final FileManifest fileManifest; + private final WriterDelegator writerDelegator; + private final List docIntegrations; + private final DocFormat docFormat; + + /** + * 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. + */ + public DocGenerationContext( + Model model, + DocSettings docSettings, + SymbolProvider symbolProvider, + FileManifest fileManifest, + List docIntegrations + ) { + this.model = model; + this.docSettings = docSettings; + this.fileManifest = fileManifest; + this.docIntegrations = docIntegrations; + + DocFormat resolvedFormat = null; + var availableFormats = new LinkedHashSet(); + for (var integration : docIntegrations) { + for (var format : integration.docFormats(docSettings)) { + if (format.name().equals(docSettings.format())) { + resolvedFormat = format; + symbolProvider = new FileExtensionDecorator(symbolProvider, resolvedFormat.extension()); + break; + } + availableFormats.add(format.name()); + } + } + if (resolvedFormat == null) { + throw new CodegenException(String.format( + "Unknown doc format `%s`. You may be missing a dependency. Currently available formats: [%s]", + docSettings.format(), String.join(", ", availableFormats) + )); + } + + this.docFormat = resolvedFormat; + this.symbolProvider = symbolProvider; + this.writerDelegator = new WriterDelegator<>(fileManifest, symbolProvider, resolvedFormat.writerFactory()); + } + + @Override + public Model model() { + return model; + } + + @Override + public DocSettings settings() { + return docSettings; + } + + @Override + public SymbolProvider symbolProvider() { + return symbolProvider; + } + + @Override + public FileManifest fileManifest() { + return fileManifest; + } + + @Override + public WriterDelegator writerDelegator() { + return writerDelegator; + } + + @Override + public List integrations() { + return docIntegrations; + } + + /** + * @return Returns the selected format that documentation should be generated in. + */ + public DocFormat docFormat() { + return this.docFormat; + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocIntegration.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocIntegration.java new file mode 100644 index 00000000000..b95e5aa9273 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocIntegration.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; + +import java.util.List; +import software.amazon.smithy.codegen.core.SmithyIntegration; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Allows integrating additional functionality into the documentation generator. + * + *

{@code DocIntegration}s are loaded as a Java SPI. To make your integration + * discoverable, add a file to {@code META-INF/services} named + * {@code software.amazon.smithy.docgen.core.DocIntegration} where each line is + * the fully-qualified class name of your integrations. Several tools, such as + * {@code AutoService}, can do this for you. + */ +@SmithyUnstableApi +public interface DocIntegration extends SmithyIntegration { + + /** + * Adds {@link DocFormat}s to the list of supported formats. + * + *

When resolving the format implementation, the first format found with a + * matching name will be used. Use {@link #priority} to adjust which integration + * is seen first. + * + * @param settings The documentation generation settings. + * @return A list of formats to add. + */ + default List docFormats(DocSettings settings) { + return List.of(); + } +} 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 new file mode 100644 index 00000000000..92acee1bd0a --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSettings.java @@ -0,0 +1,59 @@ +/* + * 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.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Settings for documentation generation. These can be set in the + * {@code smithy-build.json} configuration for this plugin. + * + * @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 references A mapping of external resources to their documentation URIs, used + * when generating links for the + * references trait + * for resources that are not contained within the model. + */ +@SmithyUnstableApi +public record DocSettings(ShapeId service, String format, Map references) { + + /** + * Settings for documentation generation. These can be set in the + * {@code smithy-build.json} configuration for this plugin. + * + * @param service The shape id of the service to generate documentation for. + * @param format The format to generate documentation in. The default is markdown. + */ + public DocSettings { + Objects.requireNonNull(service); + Objects.requireNonNull(format); + } + + /** + * Load the settings from an {@code ObjectNode}. + * + * @param pluginSettings the {@code ObjectNode} to load settings from. + * @return loaded settings based on the given node. + */ + public static DocSettings fromNode(ObjectNode pluginSettings) { + var references = pluginSettings.getObjectMember("references").orElse(ObjectNode.objectNode()) + .getMembers().entrySet().stream() + .collect(Collectors.toMap( + e -> ShapeId.from(e.getKey().getValue()), + e -> e.getValue().expectStringNode().getValue())); + return new DocSettings( + pluginSettings.expectStringMember("service").expectShapeId(), + pluginSettings.getStringMemberOrDefault("format", "sphinx-markdown"), + references + ); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSymbolProvider.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSymbolProvider.java new file mode 100644 index 00000000000..0e587b69fab --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocSymbolProvider.java @@ -0,0 +1,346 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core; + +import static java.lang.String.format; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Logger; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolProvider; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.AuthDefinitionTrait; +import software.amazon.smithy.model.traits.InputTrait; +import software.amazon.smithy.model.traits.OutputTrait; +import software.amazon.smithy.model.traits.StringTrait; +import software.amazon.smithy.model.traits.TitleTrait; +import software.amazon.smithy.model.traits.TraitDefinition; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * Creates documentation Symbols for each shape in the model. + * + *

These symbols contain many important pieces of metadata. Particularly + * important are: + * + *

    + *
  • {@code name}: The name of the symbol will be used as the title for its + * definition section. For services, this defaults to the value of the + * {@code title} trait. For other shapes, it defaults to the shape name including + * any renames from the attached service. + * + *
  • {@code definitionFile}: The file in which the documentation for this shape + * should be written. By default these are all written to a single flat directory. + * If this is empty, the shape does not have its own definition section and cannot + * be linked to. + * + *
  • {@link #SHAPE_PROPERTY}: A named Shape property containing the shape that + * the symbol represents. Decorators provided by + * {@link DocIntegration#decorateSymbolProvider} MUST set or preserve this + * property. + * + *
  • {@link #OPERATION_PROPERTY}: A named OperationShape property containing the + * operation shape that the shape is bound to. This will only be present on + * structure shapes that have the {@code input} or {@code output} traits. + * + *
  • {@link #LINK_ID_PROPERTY}: A named String property containing the string to + * use for the id for links to the shape. In HTML, this would be the {@code id} for + * the tag containing the shape's definition. Given a link id {@code foo}, a link + * to the shape's definition might look like {@code https://example.com/shapes#foo} + * for example. If this or {@code definitionFile} is empty, it is not possible to + * link to the shape. + * + *
  • {@link #ENABLE_DEFAULT_FILE_EXTENSION}: A named boolean property indicating + * whether the symbol's definition file should have the default file extension + * applied. If not present or set to {@code false}, the file extension will not be + * applied. + *
+ * + *

Decorators provided by {@link DocIntegration#decorateSymbolProvider} MUST set + * these properties or preserve + */ +@SmithyUnstableApi +public final class DocSymbolProvider extends ShapeVisitor.Default implements SymbolProvider { + + /** + * The name for a shape symbol's named property containing the shape the symbol + * represents. + * + *

Decorators provided by {@link DocIntegration#decorateSymbolProvider} MUST + * preserve this property. + * + *

Use {@code symbol.expectProperty(SHAPE_PROPERTY, Shape.class)} to access this + * property. + */ + public static final String SHAPE_PROPERTY = "shape"; + + /** + * The operation that the symbol's shape is bound to. + * + *

This property will only be present on structures that have either the + * {@code input} or {@code output} trait. + * + *

Use {@code symbol.getProperty(OPERATION_PROPERTY, OperationShape.class)} to + * access this property. + */ + public static final String OPERATION_PROPERTY = "operation"; + + /** + * The name for a shape symbol's named property containing the string to use for + * the id for links to the shape. In HTML, this would be the {@code id} for the tag + * containing the shape's definition. Given a link id {@code foo}, a link to the + * shape's definition might look like {@code https://example.com/shapes#foo} for + * example. + * + *

If this or {@code definitionFile} is empty, it is not possible to link to + * the shape. + * + *

Use {@code symbol.getProperty(LINK_ID_PROPERTY, String.class)} to access this + * property. + */ + public static final String LINK_ID_PROPERTY = "linkId"; + + /** + * A named boolean property indicating whether the symbol's definition file should + * have the default file extension applied. If not present or set to {@code false}, + * the file extension will not be applied. + * + *

Use {@code symbol.getProperty(LINK_ID_PROPERTY, Boolean.class)} to access this + * property. + */ + public static final String ENABLE_DEFAULT_FILE_EXTENSION = "enableDefaultFileExtension"; + + private static final Logger LOGGER = Logger.getLogger(DocSymbolProvider.class.getName()); + private static final String SERVICE_FILE = "index"; + + private final Model model; + private final DocSettings docSettings; + private final ServiceShape serviceShape; + private final Map ioToOperation; + + /** + * Constructor. + * + * @param model The model to provide symbols for. + * @param docSettings Settings used to customize symbol creation. + */ + public DocSymbolProvider(Model model, DocSettings docSettings) { + this.model = model; + this.docSettings = docSettings; + this.serviceShape = model.expectShape(docSettings.service(), ServiceShape.class); + this.ioToOperation = mapIoShapesToOperations(model); + } + + private Map mapIoShapesToOperations(Model model) { + // Map input and output structures to their containing shapes. These will be + // documented alongside their associated operations, so we need said operations + // when generating symbols for them. Pre-computing this mapping is a bit faster + // than just running a selector every time we hit an IO + // shape. + var operationIoMap = new HashMap(); + var operationIndex = OperationIndex.of(model); + for (var operation : model.getOperationShapes()) { + operationIndex.getInputShape(operation) + .filter(i -> i.hasTrait(InputTrait.class)) + .ifPresent(i -> operationIoMap.put(i.getId(), operation)); + operationIndex.getOutputShape(operation) + .filter(i -> i.hasTrait(OutputTrait.class)) + .ifPresent(i -> operationIoMap.put(i.getId(), operation)); + } + return Map.copyOf(operationIoMap); + } + + @Override + public Symbol toSymbol(Shape shape) { + var symbol = shape.accept(this); + LOGGER.fine(() -> format("Creating symbol from %s: %s", shape, symbol)); + return symbol; + } + + @Override + public Symbol serviceShape(ServiceShape shape) { + return getSymbolBuilder(shape) + .definitionFile(getDefinitionFile(SERVICE_FILE)) + .build(); + } + + @Override + public Symbol resourceShape(ResourceShape shape) { + return getSymbolBuilderWithFile(shape).build(); + } + + @Override + public Symbol operationShape(OperationShape shape) { + return getSymbolBuilderWithFile(shape).build(); + } + + @Override + public Symbol structureShape(StructureShape shape) { + var builder = getSymbolBuilder(shape); + if (shape.hasTrait(TraitDefinition.class)) { + if (shape.hasTrait(AuthDefinitionTrait.class)) { + builder.definitionFile(getDefinitionFile(SERVICE_FILE)); + } + return builder.build(); + } + + builder.definitionFile(getDefinitionFile(serviceShape, shape)); + if (ioToOperation.containsKey(shape.getId())) { + // Input and output structures are documented on the operation's definition page. + var operation = ioToOperation.get(shape.getId()); + builder.definitionFile(getDefinitionFile(serviceShape, operation)); + builder.putProperty(OPERATION_PROPERTY, operation); + } + return builder.build(); + } + + @Override + public Symbol enumShape(EnumShape shape) { + return getSymbolBuilderWithFile(shape).build(); + } + + @Override + public Symbol intEnumShape(IntEnumShape shape) { + return getSymbolBuilderWithFile(shape).build(); + } + + @Override + public Symbol unionShape(UnionShape shape) { + return getSymbolBuilderWithFile(shape).build(); + } + + @Override + public Symbol memberShape(MemberShape shape) { + var builder = getSymbolBuilder(shape) + .definitionFile(getDefinitionFile(serviceShape, model.expectShape(shape.getId().withoutMember()))); + + Optional containerLinkId = model.expectShape(shape.getContainer()) + .accept(this) + .getProperty(LINK_ID_PROPERTY, String.class); + if (containerLinkId.isPresent()) { + var linkId = containerLinkId.get() + "-" + getLinkId(getShapeName(serviceShape, shape)); + builder.putProperty(LINK_ID_PROPERTY, linkId); + } + return builder.build(); + } + + private Symbol.Builder getSymbolBuilder(Shape shape) { + var name = getShapeName(serviceShape, shape); + return Symbol.builder() + .name(name) + .putProperty(SHAPE_PROPERTY, shape) + .putProperty(LINK_ID_PROPERTY, getLinkId(name)) + .putProperty(ENABLE_DEFAULT_FILE_EXTENSION, true); + } + + private Symbol.Builder getSymbolBuilderWithFile(Shape shape) { + return getSymbolBuilder(shape) + .definitionFile(getDefinitionFile(serviceShape, shape)); + } + + private String getDefinitionFile(ServiceShape serviceShape, Shape shape) { + var path = getShapeName(serviceShape, shape).replaceAll("\\s+", ""); + if (shape.isResourceShape()) { + path = "resources/" + path; + } else if (shape.isOperationShape()) { + path = "operations/" + path; + } else { + path = "shapes/" + path; + } + return getDefinitionFile(path); + } + + private String getDefinitionFile(String path) { + return "content/" + path; + } + + private String getShapeName(ServiceShape serviceShape, Shape shape) { + if (shape.isServiceShape()) { + return shape.getTrait(TitleTrait.class) + .map(StringTrait::getValue) + .orElse(shape.getId().getName()); + } + if (shape.isMemberShape()) { + return toMemberName(shape.asMemberShape().get()); + } else { + return shape.getId().getName(serviceShape); + } + } + + private String getLinkId(String shapeName) { + return shapeName.toLowerCase(Locale.ENGLISH).replaceAll("\\s+", "-"); + } + + // All other shapes don't get generation, so we'll do null checks where this might + // have impact. + @Override + protected Symbol getDefault(Shape shape) { + return getSymbolBuilder(shape).build(); + } + + /** + * Adds file extensions to symbol definition files. Used with {@link DocFormat} + * by default. + * + *

Symbols can set {@link #ENABLE_DEFAULT_FILE_EXTENSION} to {@code false} to + * disable this on a per-symbol basis. + */ + public static final class FileExtensionDecorator implements SymbolProvider { + private final SymbolProvider wrapped; + private final String extension; + + /** + * Constructor. + * @param wrapped The symbol provider to wrap. + * @param extension The file extension to add. This must include any necessary periods. + */ + public FileExtensionDecorator(SymbolProvider wrapped, String extension) { + this.wrapped = Objects.requireNonNull(wrapped); + this.extension = Objects.requireNonNull(extension); + } + + @Override + public Symbol toSymbol(Shape shape) { + var symbol = wrapped.toSymbol(shape); + if (!symbol.getProperty(ENABLE_DEFAULT_FILE_EXTENSION, Boolean.class).orElse(false)) { + return symbol; + } + return symbol.toBuilder() + .definitionFile(addExtension(symbol.getDefinitionFile())) + .declarationFile(addExtension(symbol.getDeclarationFile())) + .build(); + } + + private String addExtension(String path) { + if (!StringUtils.isBlank(path) && !path.endsWith(extension)) { + path += extension; + } + return path; + } + + @Override + public String toMemberName(MemberShape shape) { + return wrapped.toMemberName(shape); + } + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocgenUtils.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocgenUtils.java new file mode 100644 index 00000000000..6556048b997 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DocgenUtils.java @@ -0,0 +1,148 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core; + +import static java.lang.String.format; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.ServiceIndex; +import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ToShapeId; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * Provides various utility methods. + */ +@SmithyUnstableApi +public final class DocgenUtils { + + private static final Logger LOGGER = Logger.getLogger(DocgenUtils.class.getName()); + + private DocgenUtils() {} + + /** + * Executes a given shell command in a given directory. + * + * @param command The string command to execute, e.g. "sphinx-build". + * @param directory The directory to run the command in. + * @return Returns the console output of the command. + */ + public static String runCommand(String command, Path directory) { + String[] finalizedCommand; + if (System.getProperty("os.name").toLowerCase().startsWith("windows")) { + finalizedCommand = new String[]{"cmd.exe", "/c", command}; + } else { + finalizedCommand = new String[]{"sh", "-c", command}; + } + + ProcessBuilder processBuilder = new ProcessBuilder(finalizedCommand) + .redirectErrorStream(true) + .directory(directory.toFile()); + + try { + Process process = processBuilder.start(); + List output = new ArrayList<>(); + + // Capture output for reporting. + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( + process.getInputStream(), Charset.defaultCharset()))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + LOGGER.finest(line); + output.add(line); + } + } + + process.waitFor(); + process.destroy(); + + String joinedOutput = String.join(System.lineSeparator(), output); + if (process.exitValue() != 0) { + throw new CodegenException(format( + "Command `%s` failed with output:%n%n%s", command, joinedOutput)); + } + return joinedOutput; + } catch (InterruptedException | IOException e) { + throw new CodegenException(e); + } + } + + /** + * Replaces all newline characters in a string with the system line separator. + * @param input The string to normalize + * @return A string with system-appropriate newlines. + */ + public static String normalizeNewlines(String input) { + return input.replaceAll("\r?\n", System.lineSeparator()); + } + + /** + * Gets a relative link pointing to a given symbol. + * + *

If the given symbol has no definition file or no + * {@link DocSymbolProvider#LINK_ID_PROPERTY}, the response will be empty. + * + * @param symbol The symbol to link to. + * @param relativeTo A path that the symbol should be relative to. This must be the + * path to the file containing the link. + * @return Optionally returns a relative link to the given symbol. + */ + public static Optional getSymbolLink(Symbol symbol, Path relativeTo) { + Optional linkId = symbol.getProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class); + var relativeToParent = relativeTo.getParent(); + if (StringUtils.isBlank(symbol.getDefinitionFile()) + || linkId.isEmpty() + || StringUtils.isBlank(linkId.get()) + || relativeToParent == null) { + return Optional.empty(); + } + return Optional.of(format( + "./%s#%s", relativeToParent.relativize(Paths.get(symbol.getDefinitionFile())), linkId.get() + )); + } + + /** + * Gets a priority-ordered list of the service's auth types. + * + *

This includes all the auth types bound to the service, not just those present + * in the {@code auth} trait. Auth types not present in the auth trait are at the + * end of the list, in alphabetical order. + * + * @param model The model being generated from. + * @param service The service being documented. + * @return returns a priority-ordered list of service auth types. + */ + public static List getPrioritizedServiceAuth(Model model, ToShapeId service) { + var index = ServiceIndex.of(model); + + // Get the effective auth schemes first and add them to an ordered set. This + // is important to do because the effective schemes are explicitly ordered in + // the model by the auth trait, and we want to document the auth options in + // that same order. + var authSchemes = new LinkedHashSet<>(index.getEffectiveAuthSchemes(service, AuthSchemeMode.MODELED).keySet()); + + // Since the auth trait can exclude some of the service's auth types, we need + // to add those in last. + authSchemes.addAll(index.getAuthSchemes(service).keySet()); + + return List.copyOf(authSchemes); + } +} 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 new file mode 100644 index 00000000000..e3c14d68b0e --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/SmithyDocPlugin.java @@ -0,0 +1,80 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.codegen.core.directed.CodegenDirector; +import software.amazon.smithy.docgen.core.validation.DocValidationEventDecorator; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.linters.InputOutputStructureReuseValidator; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.validation.ValidatedResult; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.ValidationEventDecorator; +import software.amazon.smithy.model.validation.suppressions.ModelBasedEventDecorator; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Generates API documentation from a Smithy model. + */ +@SmithyInternalApi +public final class SmithyDocPlugin implements SmithyBuildPlugin { + + private static final Logger LOGGER = Logger.getLogger(SmithyDocPlugin.class.getName()); + + @Override + public String getName() { + return "docgen"; + } + + @Override + public void execute(PluginContext pluginContext) { + LOGGER.fine("Beginning documentation generation."); + CodegenDirector runner + = new CodegenDirector<>(); + + runner.directedCodegen(new DirectedDocGen()); + runner.integrationClass(DocIntegration.class); + runner.fileManifest(pluginContext.getFileManifest()); + runner.model(getValidatedModel(pluginContext.getModel()).unwrap()); + DocSettings settings = runner.settings(DocSettings.class, pluginContext.getSettings()); + runner.service(settings.service()); + runner.performDefaultCodegenTransforms(); + runner.run(); + LOGGER.fine("Finished documentation generation."); + } + + private ValidatedResult getValidatedModel(Model model) { + // This decorator will add context for why these are particularly important for docs. + ValidationEventDecorator eventDecorator = new DocValidationEventDecorator(); + + // This will discover and apply suppressions from the model. + Optional modelDecorator = new ModelBasedEventDecorator() + .createDecorator(model).getResult(); + if (modelDecorator.isPresent()) { + eventDecorator = ValidationEventDecorator.compose(List.of(modelDecorator.get(), eventDecorator)); + } + + var events = new ArrayList(); + for (var event : validate(model)) { + if (eventDecorator.canDecorate(event)) { + event = eventDecorator.decorate(event); + } + events.add(event); + } + return new ValidatedResult<>(model, events); + } + + private List validate(Model model) { + return new InputOutputStructureReuseValidator().validate(model); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/GeneratorUtils.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/GeneratorUtils.java new file mode 100644 index 00000000000..b1b29ce7c30 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/GeneratorUtils.java @@ -0,0 +1,194 @@ +/* + * 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.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.DocSymbolProvider; +import software.amazon.smithy.docgen.core.sections.BoundOperationSection; +import software.amazon.smithy.docgen.core.sections.BoundOperationsSection; +import software.amazon.smithy.docgen.core.sections.BoundResourceSection; +import software.amazon.smithy.docgen.core.sections.BoundResourcesSection; +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.sections.ProtocolsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.ListType; +import software.amazon.smithy.model.knowledge.ServiceIndex; +import software.amazon.smithy.model.shapes.EntityShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyInternalApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * Provides common generation methods for services and resources. + */ +@SmithyInternalApi +final class GeneratorUtils { + private GeneratorUtils() {} + + static void generateOperationListing( + DocGenerationContext context, + DocWriter writer, + EntityShape shape, + List operations + ) { + writer.pushState(new BoundOperationsSection(context, shape, operations)); + + if (operations.isEmpty()) { + writer.popState(); + return; + } + + var parentLinkId = context.symbolProvider().toSymbol(shape) + .expectProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class); + writer.openHeading("Operations", parentLinkId + "-operations"); + writer.openList(ListType.UNORDERED); + + for (var operation : operations) { + writer.pushState(new BoundOperationSection(context, shape, operation)); + writeListingElement(context, writer, operation); + writer.popState(); + } + + writer.closeList(ListType.UNORDERED); + writer.closeHeading(); + writer.popState(); + } + + static void generateResourceListing( + DocGenerationContext context, + DocWriter writer, + EntityShape shape, + List resources + ) { + writer.pushState(new BoundResourcesSection(context, shape, resources)); + + if (resources.isEmpty()) { + writer.popState(); + return; + } + + var parentLinkId = context.symbolProvider().toSymbol(shape) + .expectProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class); + var heading = shape.isServiceShape() ? "Resources" : "Sub-Resources"; + writer.openHeading(heading, parentLinkId + "-" + heading.toLowerCase(Locale.ENGLISH)); + writer.openList(ListType.UNORDERED); + + for (var resource : resources) { + writer.pushState(new BoundResourceSection(context, shape, resource)); + writeListingElement(context, writer, resource); + writer.popState(); + } + + writer.closeList(ListType.UNORDERED); + writer.closeHeading(); + writer.popState(); + } + + private static void writeListingElement(DocGenerationContext context, DocWriter writer, Shape shape) { + writer.openListItem(ListType.UNORDERED); + var symbol = context.symbolProvider().toSymbol(shape); + writer.writeInline("$R: ", symbol).writeShapeDocs(shape, context.model()); + writer.closeListItem(ListType.UNORDERED); + } + + static void writeProtocolsSection(DocGenerationContext context, DocWriter writer, Shape shape) { + var protocols = ServiceIndex.of(context.model()).getProtocols(context.settings().service()).keySet(); + if (protocols.isEmpty()) { + return; + } + writer.pushState(new ProtocolsSection(context, shape)); + + AtomicReference tabGroupContents = new AtomicReference<>(); + var tabGroup = capture(writer, tabGroupWriter -> { + tabGroupWriter.openTabGroup(); + tabGroupContents.set(capture(tabGroupWriter, w -> { + for (var protocol : protocols) { + writeProtocolSection(context, w, shape, protocol); + } + })); + tabGroupWriter.closeTabGroup(); + }); + + if (StringUtils.isBlank(tabGroupContents.get())) { + // The extra newline is needed because the section intercepting logic actually adds one + // by virtue of calling write instead of writeInline + writer.unwrite("$L\n", tabGroup); + } + + writer.popState(); + } + + private static void writeProtocolSection( + DocGenerationContext context, + DocWriter writer, + Shape shape, + ShapeId protocol + ) { + var protocolSymbol = context.symbolProvider().toSymbol(context.model().expectShape(protocol)); + + AtomicReference tabContents = new AtomicReference<>(); + var tab = capture(writer, tabWriter -> { + tabWriter.openTab(protocolSymbol.getName()); + tabContents.set(capture(tabWriter, w2 -> tabWriter.injectSection( + new ProtocolSection(context, shape, protocol)))); + tabWriter.closeTab(); + }); + + if (StringUtils.isBlank(tabContents.get())) { + // The extra newline is needed because the section intercepting logic actually adds one + // by virtue of calling write instead of writeInline + writer.unwrite("$L\n", tab); + } + } + + /** + * Captures and returns what is written by the given consumer. + * + * @param writer The writer to capture from. + * @param consumer A consumer that writes text to be captured. + * @return Returns what was written by the consumer. + */ + private static String capture(DocWriter writer, Consumer consumer) { + var recorder = new RecordingInterceptor(); + writer.pushState(new CapturingSection()).onSection(recorder); + consumer.accept(writer); + writer.popState(); + return recorder.getContents(); + } + + private record CapturingSection() implements CodeSection {} + + /** + * Records what was written to the section previously and writes it back. + */ + private static final class RecordingInterceptor implements CodeInterceptor { + private String contents = null; + + public String getContents() { + return contents; + } + + @Override + public Class sectionType() { + return CapturingSection.class; + } + + @Override + public void write(DocWriter writer, String previousText, CapturingSection section) { + contents = previousText; + writer.writeWithNoFormatting(previousText); + } + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/MemberGenerator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/MemberGenerator.java new file mode 100644 index 00000000000..eb4759eed1a --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/MemberGenerator.java @@ -0,0 +1,389 @@ +/* + * 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.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.DocSymbolProvider; +import software.amazon.smithy.docgen.core.sections.MemberSection; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.sections.ShapeMembersSection; +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.BigDecimalShape; +import software.amazon.smithy.model.shapes.BigIntegerShape; +import software.amazon.smithy.model.shapes.BlobShape; +import software.amazon.smithy.model.shapes.BooleanShape; +import software.amazon.smithy.model.shapes.ByteShape; +import software.amazon.smithy.model.shapes.DocumentShape; +import software.amazon.smithy.model.shapes.DoubleShape; +import software.amazon.smithy.model.shapes.EnumShape; +import software.amazon.smithy.model.shapes.FloatShape; +import software.amazon.smithy.model.shapes.IntEnumShape; +import software.amazon.smithy.model.shapes.IntegerShape; +import software.amazon.smithy.model.shapes.ListShape; +import software.amazon.smithy.model.shapes.LongShape; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.ShapeVisitor; +import software.amazon.smithy.model.shapes.ShortShape; +import software.amazon.smithy.model.shapes.StringShape; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.TimestampShape; +import software.amazon.smithy.model.shapes.UnionShape; +import software.amazon.smithy.model.traits.EnumValueTrait; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * Generates documentation for shape members. + * + *

The output of this can be customized in a number of ways. To add details to + * or re-write particular sections, register an interceptor with + * {@link software.amazon.smithy.docgen.core.DocIntegration#interceptors}. The following + * sections will be present: + * + *

    + *
  • {@link MemberSection}: Enables re-writing the documentation for specific members. + * + *
  • {@link ShapeMembersSection}: Enables re-writing or overwriting the entire list + * of members, including changes made in other sections. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.ProtocolSection} Enables adding + * traits that are specific to a particular protocol. This section will only be present if + * there are protocol traits applied to the service. If there are multiple protocol traits, + * this section will appear once per protocol. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.ProtocolsSection} Enables + * modifying the tab group containing all the protocol traits for all the protocols. + *
+ * + *

To change the intermediate format (e.g. from markdown to restructured text), + * a new {@link software.amazon.smithy.docgen.core.DocFormat} needs to be introduced + * via {@link software.amazon.smithy.docgen.core.DocIntegration#docFormats}. + */ +@SmithyUnstableApi +public final class MemberGenerator implements Runnable { + + private final DocGenerationContext context; + private final Shape shape; + private final MemberListingType listingType; + private final DocWriter writer; + + /** + * Constructs a MemberGenerator. + * + * @param context The context used to generate documentation. + * @param writer The writer to write to. + * @param shape The shape whose members are being generated. + * @param listingType The type of listing being generated. + */ + public MemberGenerator( + DocGenerationContext context, + DocWriter writer, + Shape shape, + MemberListingType listingType + ) { + this.context = context; + this.writer = writer; + this.shape = shape; + this.listingType = listingType; + } + + @Override + public void run() { + var members = getMembers(); + writer.pushState(new ShapeMembersSection(context, shape, members, listingType)); + var parentSymbol = context.symbolProvider().toSymbol(shape); + if (!members.isEmpty()) { + parentSymbol.getProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class).ifPresent(linkId -> { + writer.writeAnchor(linkId + "-" + listingType.getLinkIdSuffix()); + }); + writer.openHeading(listingType.getTitle()); + writer.openDefinitionList(); + for (MemberShape member : members) { + writer.pushState(new MemberSection(context, member)); + + var symbol = context.symbolProvider().toSymbol(member); + var target = context.model().expectShape(member.getTarget()); + + var typeWriter = writer.consumer(w -> target.accept(new MemberTypeVisitor(w, context, member))); + writer.openDefinitionListItem(w -> w.writeInline("$L ($C)", symbol.getName(), typeWriter)); + + writer.injectSection(new ShapeSubheadingSection(context, member)); + writer.writeShapeDocs(member, context.model()); + writer.injectSection(new ShapeDetailsSection(context, member)); + GeneratorUtils.writeProtocolsSection(context, writer, member); + writer.closeDefinitionListItem(); + writer.popState(); + } + writer.closeDefinitionList(); + writer.closeHeading(); + } + writer.popState(); + } + + private Collection getMembers() { + return switch (listingType) { + case INPUT -> context.model() + .expectShape(shape.asOperationShape().get().getInputShape()) + .getAllMembers().values(); + case OUTPUT -> context.model() + .expectShape(shape.asOperationShape().get().getOutputShape()) + .getAllMembers().values(); + case RESOURCE_IDENTIFIERS -> synthesizeResourceMembers(shape.asResourceShape().get().getIdentifiers()); + case RESOURCE_PROPERTIES -> synthesizeResourceMembers(shape.asResourceShape().get().getProperties()); + default -> shape.getAllMembers().values(); + }; + } + + // Resource identifiers and properties aren't actually members, but they're close + // enough that we can treat them like they are for the purposes of the doc generator. + private List synthesizeResourceMembers(Map properties) { + return properties.entrySet().stream() + .map(entry -> MemberShape.builder() + .id(shape.getId().withMember(entry.getKey())) + .target(entry.getValue()) + .build()) + .toList(); + } + + /** + * The type of listing. This controls the heading title and anchor id for the section. + */ + public enum MemberListingType { + /** + * Indicates the listing is for normal shape members. + */ + MEMBERS("Members"), + + /** + * Indicates the listing is for an operation's input members. + */ + INPUT("Request Members"), + + /** + * Indicates the listing is for an operation's output members. + */ + OUTPUT("Response Members"), + + /** + * Indicates the listing is for enums, intEnums, or unions, which each only + * allow one of their members to be selected. + */ + OPTIONS("Options"), + + /** + * Indicates the listing is for a resource's identifiers. + */ + RESOURCE_IDENTIFIERS("Identifiers"), + + /** + * Indicates the listing is for a resource's modeled properties. + */ + RESOURCE_PROPERTIES("Properties"); + + private final String title; + private final String linkIdSuffix; + + MemberListingType(String title) { + this.title = title; + this.linkIdSuffix = title.toLowerCase(Locale.ENGLISH).strip().replaceAll("\\s+", "-"); + } + + /** + * @return returns the heading title that should be used for the listing. + */ + public String getTitle() { + return title; + } + + /** + * @return returns the suffix that will be applied to the parent shape's link + * id to form this member listing's link id. + */ + public String getLinkIdSuffix() { + return linkIdSuffix; + } + } + + private static class MemberTypeVisitor extends ShapeVisitor.Default { + + private final DocWriter writer; + private final DocGenerationContext context; + private final MemberShape member; + + MemberTypeVisitor(DocWriter writer, DocGenerationContext context, MemberShape member) { + this.writer = writer; + this.context = context; + this.member = member; + } + + @Override + protected Void getDefault(Shape shape) { + throw new CodegenException(String.format( + "Unexpected member %s of type %s", shape.getId(), shape.getType())); + } + + @Override + public Void blobShape(BlobShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void booleanShape(BooleanShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void byteShape(ByteShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void shortShape(ShortShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void integerShape(IntegerShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void intEnumShape(IntEnumShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void longShape(LongShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void floatShape(FloatShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void documentShape(DocumentShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void doubleShape(DoubleShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void bigIntegerShape(BigIntegerShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void bigDecimalShape(BigDecimalShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void stringShape(StringShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void enumShape(EnumShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void timestampShape(TimestampShape shape) { + writeShapeName(shape); + return null; + } + + @Override + public Void listShape(ListShape shape) { + writer.writeInline("List\\<"); + context.model().expectShape(shape.getMember().getTarget()).accept(this); + writer.writeInline("\\>"); + return null; + } + + @Override + public Void mapShape(MapShape shape) { + writer.writeInline("Map\\<"); + context.model().expectShape(shape.getKey().getTarget()).accept(this); + writer.writeInline(", "); + context.model().expectShape(shape.getValue().getTarget()).accept(this); + writer.writeInline("\\>"); + return null; + } + + @Override + public Void structureShape(StructureShape shape) { + if (member.hasTrait(EnumValueTrait.class)) { + var trait = member.expectTrait(EnumValueTrait.class); + if (trait.getIntValue().isPresent()) { + writer.writeInline("$`", trait.expectIntValue()); + } else { + writer.writeInline("$`", trait.expectStringValue()); + } + } else { + writeShapeName(shape); + } + return null; + } + + @Override + public Void unionShape(UnionShape shape) { + writeShapeName(shape); + return null; + } + + private void writeShapeName(Shape shape) { + var symbol = context.symbolProvider().toSymbol(shape); + + if (StringUtils.isNotBlank(symbol.getDefinitionFile())) { + writer.writeInline("$R", symbol); + } else { + // If the symbol doesn't have a definition file, it can't be linked. + // If it can't be linked to, then the actual name of the shape + // doesn't matter and would only serve as a confusing dead reference + // if displayed in the docs. Instead we just use the shape type name, + // which should be more clear in almost every case. A SymbolReference + // is passed along rather than writing a literal string so that + // implementations can do something with the source symbol if + // necessary. + var reference = SymbolReference.builder() + .symbol(symbol) + .alias(StringUtils.capitalize(shape.getType().name().toLowerCase(Locale.ENGLISH))) + .build(); + writer.writeInline("$R", reference); + } + } + } +} 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 new file mode 100644 index 00000000000..edf1e2765c8 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/OperationGenerator.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.docgen.core.generators; + +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.directed.GenerateOperationDirective; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.DocSettings; +import software.amazon.smithy.docgen.core.DocSymbolProvider; +import software.amazon.smithy.docgen.core.generators.MemberGenerator.MemberListingType; +import software.amazon.smithy.docgen.core.sections.ErrorsSection; +import software.amazon.smithy.docgen.core.sections.ExampleSection; +import software.amazon.smithy.docgen.core.sections.ExamplesSection; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.sections.ShapeSection; +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; +import software.amazon.smithy.model.traits.ExamplesTrait.Example; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Generates documentation for operations. + * + *

The output of this can be customized in a number of ways. To add details to + * or re-write particular sections, register an interceptor with + * {@link software.amazon.smithy.docgen.core.DocIntegration#interceptors}. The following + * sections are guaranteed to be present: + * + *

    + *
  • {@link ShapeSubheadingSection}: Enables adding additional details that are + * inserted right after the shape's heading, before modeled docs. + * + *
  • {@link ShapeDetailsSection}: Enables adding additional details that are inserted + * directly after the shape's modeled documentation. + * + *
  • {@link ShapeSection}: Three versions of this section will appear on the page. + * The first is for the operation shape itself, which enables re-writing or adding + * details to the entire page. The other two are for the input and output shapes, + * which enable modifying the documentation for just the input and output sections. + * + *
  • {@link ErrorsSection}: This section will contain a listing of all the errors + * the operation might return. If a synthetic error needs to be applied to an + * operation, it is better to simply add it to the shape with + * {@link software.amazon.smithy.docgen.core.DocIntegration#preprocessModel}. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.ProtocolSection} Enables adding + * traits that are specific to a particular protocol. This section will only be present if + * there are protocol traits applied to the service. If there are multiple protocol traits, + * this section will appear once per protocol. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.ProtocolsSection} Enables + * modifying the tab group containing all the protocol traits for all the protocols. + *
+ * + * Additionally, if the operation's input or output shapes have members the following + * sections will also be present: + * + *
    + *
  • {@link software.amazon.smithy.docgen.core.sections.MemberSection}: enables + * modifying documentation for an individual input or output member. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.ShapeMembersSection}: + * Two versions of this section will appear on the page, one for the operation's + * input shape members and one for the operation's output shape members. These + * enable re-writing or editing those sections. + *
+ * + * If the {@code examples} trait has been applied to the operation, it will also have + * the following sections: + * + *
    + *
  • {@link ExamplesSection}: enables modifying the entire examples section. + * + *
  • {@link ExampleSection}: enables modifying a singular example, including the + * snippets in every discovered language. + *
+ * + *

To change the intermediate format (e.g. from markdown to restructured text), + * a new {@link software.amazon.smithy.docgen.core.DocFormat} needs to be introduced + * via {@link software.amazon.smithy.docgen.core.DocIntegration#docFormats}. + * + * @see MemberGenerator for more details on how member documentation is generated. + */ +@SmithyInternalApi +public final class OperationGenerator + implements Consumer> { + @Override + public void accept(GenerateOperationDirective directive) { + var operation = directive.shape(); + var context = directive.context(); + var symbol = directive.symbolProvider().toSymbol(operation); + context.writerDelegator().useShapeWriter(directive.shape(), writer -> { + writer.pushState(new ShapeSection(context, operation)); + var linkId = symbol.expectProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class); + writer.openHeading(symbol.getName(), linkId); + writer.injectSection(new ShapeSubheadingSection(context, operation)); + writer.writeShapeDocs(operation, directive.model()); + writer.injectSection(new ShapeDetailsSection(context, operation)); + GeneratorUtils.writeProtocolsSection(context, writer, operation); + + new MemberGenerator(context, writer, operation, MemberListingType.INPUT).run(); + new MemberGenerator(context, writer, operation, MemberListingType.OUTPUT).run(); + + writeErrors(context, writer, directive.service(), operation, linkId); + + var examples = operation.getTrait(ExamplesTrait.class).map(ExamplesTrait::getExamples).orElse(List.of()); + writeExamples(context, writer, operation, examples, linkId); + + writer.closeHeading(); + writer.popState(); + }); + } + + private void writeErrors( + DocGenerationContext context, + DocWriter writer, + ServiceShape service, + OperationShape operation, + String linkId + ) { + var errors = operation.getErrors(service); + writer.pushState(new ErrorsSection(context, operation)); + if (!errors.isEmpty()) { + writer.openHeading("Errors", linkId + "-errors"); + writer.write("This operation may return any of the following errors:"); + writer.openList(ListType.UNORDERED); + for (var error : errors) { + var errorShape = context.model().expectShape(error); + writer.openListItem(ListType.UNORDERED); + writer.writeInline("$R: ", context.symbolProvider().toSymbol(errorShape)); + writer.writeShapeDocs(errorShape, context.model()); + writer.closeListItem(ListType.UNORDERED); + } + writer.closeList(ListType.UNORDERED); + writer.closeHeading(); + } + writer.popState(); + } + + private void writeExamples( + DocGenerationContext context, + DocWriter writer, + OperationShape operation, + List examples, + String operationLinkId + ) { + writer.pushState(new ExamplesSection(context, operation, examples)); + if (examples.isEmpty()) { + writer.popState(); + return; + } + + writer.openHeading("Examples", operationLinkId + "-examples"); + for (var example : examples) { + 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(); + + writer.closeTabGroup(); + + writer.closeHeading(); + writer.popState(); + } + writer.closeHeading(); + writer.popState(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ResourceGenerator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ResourceGenerator.java new file mode 100644 index 00000000000..66c669a8822 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ResourceGenerator.java @@ -0,0 +1,204 @@ +/* + * 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.util.HashSet; +import java.util.Locale; +import java.util.function.BiConsumer; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.DocSymbolProvider; +import software.amazon.smithy.docgen.core.generators.MemberGenerator.MemberListingType; +import software.amazon.smithy.docgen.core.sections.LifecycleOperationSection; +import software.amazon.smithy.docgen.core.sections.LifecycleOperationSection.LifecycleType; +import software.amazon.smithy.docgen.core.sections.LifecycleSection; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.sections.ShapeSection; +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.utils.SmithyInternalApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * Generates documentation for the resources. + * + *

The output of this can be customized in a number of ways. To add details to + * or re-write particular sections, register an interceptor with + * {@link software.amazon.smithy.docgen.core.DocIntegration#interceptors}. The following + * sections are guaranteed to be present: + * + *

    + *
  • {@link ShapeSection}: Enables re-writing or overwriting the entire page, + * including changes made in other sections. + * + *
  • {@link ShapeSubheadingSection}: Enables adding additional details that are + * inserted right after the resource's heading, before modeled docs. + * + *
  • {@link ShapeDetailsSection}: Enables adding in additional details that are + * inserted after the resource's modeled documentation. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.ShapeMembersSection}: + * Two versions of this section will appear on the page, one for the resource's + * identifiers and one for the resource's properties. These enable re-writing or + * editing those entire sections. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.MemberSection}: enables + * modifying documentation for an individual resource property or identifier. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.BoundOperationsSection}: + * enables modifying the listing of operations directly bound to the resource. This + * does not include any operations transitively bound to the resource through + * sub-resources or lifecycle operations. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.BoundOperationSection}: + * enables modifying the listing of an individual operation bound to the resource. + * This does not include any operations transitively bound to the resource through + * sub-resources or lifecycle operations. This section will only be present if + * there are operations directly bound to the resource. + * + *
  • {@link LifecycleSection}: enables modifying the listing of operations bound + * as one of the resource's lifecycle operations. + * + *
  • {@link LifecycleOperationSection}: enables modifying the listing of an + * individual resource lifecycle operation. This section will only be present if + * the resource has bound lifecycle operations. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.BoundResourcesSection}: + * enables modifying the listing of sub-resources directly bound to the resource. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.BoundResourceSection}: + * enables modifying the listing of an individual sub-resource directly bound to + * the resource. This section will only be present if the resource has any + * sub-resources. + *
+ * + *

To change the intermediate format (e.g. from markdown to restructured text), + * a new {@link software.amazon.smithy.docgen.core.DocFormat} needs to be introduced + * via {@link software.amazon.smithy.docgen.core.DocIntegration#docFormats}. + * + *

To change the filename or title, implement + * {@link software.amazon.smithy.docgen.core.DocIntegration#decorateSymbolProvider} + * and modify the generated symbol's definition file. See + * {@link software.amazon.smithy.docgen.core.DocSymbolProvider} for details on other + * symbol-driven configuration options. + * + * @see + * Smithy resource shape docs. + */ +@SmithyInternalApi +public final class ResourceGenerator implements BiConsumer { + @Override + public void accept(DocGenerationContext context, ResourceShape resource) { + var symbol = context.symbolProvider().toSymbol(resource); + + context.writerDelegator().useShapeWriter(resource, writer -> { + writer.pushState(new ShapeSection(context, resource)); + var linkId = symbol.expectProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class); + writer.openHeading(symbol.getName(), linkId); + writer.injectSection(new ShapeSubheadingSection(context, resource)); + writer.writeShapeDocs(resource, context.model()); + writer.injectSection(new ShapeDetailsSection(context, resource)); + GeneratorUtils.writeProtocolsSection(context, writer, resource); + + new MemberGenerator(context, writer, resource, MemberListingType.RESOURCE_IDENTIFIERS).run(); + new MemberGenerator(context, writer, resource, MemberListingType.RESOURCE_PROPERTIES).run(); + + var subResources = resource.getResources().stream().sorted() + .map(id -> context.model().expectShape(id, ResourceShape.class)) + .toList(); + GeneratorUtils.generateResourceListing(context, writer, resource, subResources); + + generateLifecycleDocs(context, writer, resource); + + var operationIds = new HashSet<>(resource.getOperations()); + operationIds.addAll(resource.getCollectionOperations()); + var operations = operationIds.stream().sorted() + .map(id -> context.model().expectShape(id, OperationShape.class)) + .toList(); + GeneratorUtils.generateOperationListing(context, writer, resource, operations); + + writer.closeHeading(); + writer.popState(); + }); + } + + private void generateLifecycleDocs(DocGenerationContext context, DocWriter writer, ResourceShape resource) { + writer.pushState(new LifecycleSection(context, resource)); + if (!hasLifecycleBindings(resource)) { + writer.popState(); + return; + } + var linkId = context.symbolProvider().toSymbol(resource) + .expectProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class); + writer.openHeading("Lifecycle Operations", linkId + "-lifecycle-operations"); + writer.openDefinitionList(); + + if (resource.getPut().isPresent()) { + var put = context.model().expectShape(resource.getPut().get(), OperationShape.class); + writeLifecycleListing(context, writer, resource, put, LifecycleType.PUT); + } + + if (resource.getCreate().isPresent()) { + var create = context.model().expectShape(resource.getCreate().get(), OperationShape.class); + writeLifecycleListing(context, writer, resource, create, LifecycleType.CREATE); + } + + if (resource.getRead().isPresent()) { + var read = context.model().expectShape(resource.getRead().get(), OperationShape.class); + writeLifecycleListing(context, writer, resource, read, LifecycleType.READ); + } + + if (resource.getUpdate().isPresent()) { + var update = context.model().expectShape(resource.getUpdate().get(), OperationShape.class); + writeLifecycleListing(context, writer, resource, update, LifecycleType.UPDATE); + } + + if (resource.getDelete().isPresent()) { + var delete = context.model().expectShape(resource.getDelete().get(), OperationShape.class); + writeLifecycleListing(context, writer, resource, delete, LifecycleType.DELETE); + } + + if (resource.getList().isPresent()) { + var list = context.model().expectShape(resource.getList().get(), OperationShape.class); + writeLifecycleListing(context, writer, resource, list, LifecycleType.LIST); + } + + writer.closeDefinitionList(); + writer.closeHeading(); + writer.popState(); + } + + private boolean hasLifecycleBindings(ResourceShape resource) { + return resource.getPut().isPresent() + || resource.getCreate().isPresent() + || resource.getRead().isPresent() + || resource.getUpdate().isPresent() + || resource.getDelete().isPresent() + || resource.getList().isPresent(); + } + + private void writeLifecycleListing( + DocGenerationContext context, + DocWriter writer, + ResourceShape resource, + OperationShape operation, + LifecycleType lifecycleType + ) { + writer.pushState(new LifecycleOperationSection(context, resource, operation, lifecycleType)); + var lifecycleName = StringUtils.capitalize(lifecycleType.name().toLowerCase(Locale.ENGLISH)); + var operationName = context.symbolProvider().toSymbol(operation).getName(); + var reference = SymbolReference.builder() + .symbol(context.symbolProvider().toSymbol(operation)) + .alias(String.format("%s (%s)", lifecycleName, operationName)) + .build(); + writer.openDefinitionListItem(w -> w.writeInline("$R", reference)); + writer.writeShapeDocs(operation, context.model()); + writer.closeDefinitionListItem(); + writer.popState(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ServiceGenerator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ServiceGenerator.java new file mode 100644 index 00000000000..4df6cf9a18b --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/ServiceGenerator.java @@ -0,0 +1,150 @@ +/* + * 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.util.function.Consumer; +import software.amazon.smithy.codegen.core.directed.GenerateServiceDirective; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.DocSettings; +import software.amazon.smithy.docgen.core.DocgenUtils; +import software.amazon.smithy.docgen.core.sections.AuthSection; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.sections.ShapeSection; +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.knowledge.ServiceIndex; +import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.model.traits.synthetic.NoAuthTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Generates top-level documentation for the service. + * + *

The output of this can be customized in a number of ways. To add details to + * or re-write particular sections, register an interceptor with + * {@link software.amazon.smithy.docgen.core.DocIntegration#interceptors}. The following + * sections are guaranteed to be present: + * + *

    + *
  • {@link ShapeSection}: Enables re-writing or overwriting the entire page, + * including changes made in other sections. + * + *
  • {@link ShapeSubheadingSection}: Enables adding additional details that are + * inserted right after the shape's heading, before modeled docs. + * + *
  • {@link ShapeDetailsSection}: Enables adding in additional details that are + * inserted after the service's modeled documentation. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.BoundOperationsSection}: + * enables modifying the listing of operations transitively bound to the service, + * which includes operations bound to resources. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.BoundOperationSection}: + * enables modifying the listing of an individual operation transitively bound to + * the service. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.BoundResourcesSection}: + * enables modifying the listing of resources directly bound to the service. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.BoundResourceSection}: + * enables modifying the listing of an individual resource directly bound to + * the service. + * + *
  • {@link AuthSection} enables modifying the documentation for the different + * auth schemes available on the service. This section will not be present if + * the service has no auth traits. + *
+ * + *

To change the intermediate format (e.g. from markdown to restructured text), + * a new {@link software.amazon.smithy.docgen.core.DocFormat} needs to be introduced + * via {@link software.amazon.smithy.docgen.core.DocIntegration#docFormats}. + * + *

To change the filename or title, implement + * {@link software.amazon.smithy.docgen.core.DocIntegration#decorateSymbolProvider} + * and modify the generated symbol's definition file. See + * {@link software.amazon.smithy.docgen.core.DocSymbolProvider} for details on other + * symbol-driven configuration options. + * + * @see + * Smithy service shape docs. + */ +@SmithyInternalApi +public final class ServiceGenerator implements Consumer> { + + @Override + public void accept(GenerateServiceDirective directive) { + var service = directive.service(); + var context = directive.context(); + var serviceSymbol = directive.symbolProvider().toSymbol(service); + + directive.context().writerDelegator().useShapeWriter(service, writer -> { + writer.pushState(new ShapeSection(context, service)); + writer.openHeading(serviceSymbol.getName()); + writer.injectSection(new ShapeSubheadingSection(context, service)); + writer.writeShapeDocs(service, directive.model()); + writer.injectSection(new ShapeDetailsSection(context, service)); + + var topDownIndex = TopDownIndex.of(context.model()); + + // TODO: topographically sort resources + var resources = topDownIndex.getContainedResources(service).stream().sorted().toList(); + GeneratorUtils.generateResourceListing(context, writer, service, resources); + + var operations = topDownIndex.getContainedOperations(service).stream().sorted().toList(); + GeneratorUtils.generateOperationListing(context, writer, service, operations); + + writeAuthSection(context, writer, service); + + writer.closeHeading(); + writer.popState(); + }); + } + + private void writeAuthSection(DocGenerationContext context, DocWriter writer, ServiceShape service) { + var authSchemes = DocgenUtils.getPrioritizedServiceAuth(context.model(), service); + if (authSchemes.isEmpty()) { + return; + } + + writer.pushState(new AuthSection(context, service)); + writer.openHeading("Auth"); + + var index = ServiceIndex.of(context.model()); + writer.putContext("optional", index.getEffectiveAuthSchemes(service, AuthSchemeMode.NO_AUTH_AWARE) + .containsKey(NoAuthTrait.ID)); + writer.putContext("multipleSchemes", authSchemes.size() > 1); + writer.write(""" + Operations on the service ${?optional}may optionally${/optional}${^optional}MUST${/optional} \ + be called with ${?multipleSchemes}one of the following priority-ordered auth schemes${/multipleSchemes}\ + ${^multipleSchemes}the following auth scheme${/multipleSchemes}. Additionally, authentication for \ + individual operations may be optional${?multipleSchemes}, have a different priority order, support \ + fewer schemes,${/multipleSchemes} or be disabled entirely. + """); + + writer.openDefinitionList(); + + for (var scheme : authSchemes) { + var authTraitShape = context.model().expectShape(scheme); + var authTraitSymbol = context.symbolProvider().toSymbol(authTraitShape); + + writer.pushState(new ShapeSection(context, authTraitShape)); + writer.openDefinitionListItem(w -> w.write("$R", authTraitSymbol)); + + writer.injectSection(new ShapeSubheadingSection(context, authTraitShape)); + writer.writeShapeDocs(authTraitShape, context.model()); + writer.injectSection(new ShapeDetailsSection(context, authTraitShape)); + + writer.closeDefinitionListItem(); + writer.popState(); + } + + writer.closeDefinitionList(); + writer.closeHeading(); + writer.popState(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/StructuredShapeGenerator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/StructuredShapeGenerator.java new file mode 100644 index 00000000000..5af63c26857 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/StructuredShapeGenerator.java @@ -0,0 +1,95 @@ +/* + * 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.util.function.BiConsumer; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.DocSymbolProvider; +import software.amazon.smithy.docgen.core.generators.MemberGenerator.MemberListingType; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.sections.ShapeSection; +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Generates documentation for shapes with members. + * + *

The output of this can be customized in a number of ways. To add details to + * or re-write particular sections, register an interceptor with + * {@link software.amazon.smithy.docgen.core.DocIntegration#interceptors}. The following + * sections are guaranteed to be present: + * + *

    + *
  • {@link ShapeSubheadingSection}: Enables adding additional details that are + * inserted right after the shape's heading, before modeled docs. + * + *
  • {@link ShapeDetailsSection}: Enables adding additional details that are inserted + * directly after the shape's modeled documentation. + * + *
  • {@link ShapeSection}: Enables re-writing or overwriting the entire page, + * including changes made in other sections. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.ProtocolSection} Enables adding + * traits that are specific to a particular protocol. This section will only be present if + * there are protocol traits applied to the service. If there are multiple protocol traits, + * this section will appear once per protocol. This section will also appear for each member. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.ProtocolsSection} Enables + * modifying the tab group containing all the protocol traits for all the protocols. This + * section will also appear for each member. + *
+ * + * Additionally, if the shape has members the following sections will also be present: + * + *
    + *
  • {@link software.amazon.smithy.docgen.core.sections.MemberSection}: enables + * modifying documentation for an individual shape member. + * + *
  • {@link software.amazon.smithy.docgen.core.sections.ShapeMembersSection}: + * enables modifying the documentation for all of the shape's members. + *
+ * + *

To change the intermediate format (e.g. from markdown to restructured text), + * a new {@link software.amazon.smithy.docgen.core.DocFormat} needs to be introduced + * via {@link software.amazon.smithy.docgen.core.DocIntegration#docFormats}. + * + * @see MemberGenerator for more details on how member documentation is generated. + */ +@SmithyInternalApi +public final class StructuredShapeGenerator implements BiConsumer { + + private final DocGenerationContext context; + + /** + * Constructs a StructuredShapeGenerator. + * + * @param context The context used to generate documentation. + */ + public StructuredShapeGenerator(DocGenerationContext context) { + this.context = context; + } + + @Override + public void accept(Shape shape, MemberListingType listingType) { + var symbol = context.symbolProvider().toSymbol(shape); + context.writerDelegator().useShapeWriter(shape, writer -> { + writer.pushState(new ShapeSection(context, shape)); + symbol.getProperty(DocSymbolProvider.LINK_ID_PROPERTY, String.class).ifPresent(writer::writeAnchor); + writer.openHeading(symbol.getName()); + + writer.injectSection(new ShapeSubheadingSection(context, shape)); + writer.writeShapeDocs(shape, context.model()); + writer.injectSection(new ShapeDetailsSection(context, shape)); + GeneratorUtils.writeProtocolsSection(context, writer, shape); + + new MemberGenerator(context, writer, shape, listingType).run(); + + writer.closeHeading(); + writer.popState(); + }); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/integrations/BuiltinsIntegration.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/integrations/BuiltinsIntegration.java new file mode 100644 index 00000000000..e7269b2f6f1 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/integrations/BuiltinsIntegration.java @@ -0,0 +1,144 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.integrations; + +import java.util.List; +import software.amazon.smithy.docgen.core.DocFormat; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.DocIntegration; +import software.amazon.smithy.docgen.core.DocSettings; +import software.amazon.smithy.docgen.core.interceptors.ApiKeyAuthInterceptor; +import software.amazon.smithy.docgen.core.interceptors.DefaultValueInterceptor; +import software.amazon.smithy.docgen.core.interceptors.DeprecatedInterceptor; +import software.amazon.smithy.docgen.core.interceptors.EndpointInterceptor; +import software.amazon.smithy.docgen.core.interceptors.ErrorFaultInterceptor; +import software.amazon.smithy.docgen.core.interceptors.ExternalDocsInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HostLabelInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpChecksumRequiredInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpErrorInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpHeaderInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpLabelInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpPayloadInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpPrefixHeadersInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpQueryInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpQueryParamsInterceptor; +import software.amazon.smithy.docgen.core.interceptors.HttpResponseCodeInterceptor; +import software.amazon.smithy.docgen.core.interceptors.IdempotencyInterceptor; +import software.amazon.smithy.docgen.core.interceptors.InternalInterceptor; +import software.amazon.smithy.docgen.core.interceptors.JsonNameInterceptor; +import software.amazon.smithy.docgen.core.interceptors.LengthInterceptor; +import software.amazon.smithy.docgen.core.interceptors.MediaTypeInterceptor; +import software.amazon.smithy.docgen.core.interceptors.NoReplaceBindingInterceptor; +import software.amazon.smithy.docgen.core.interceptors.NoReplaceOperationInterceptor; +import software.amazon.smithy.docgen.core.interceptors.NullabilityInterceptor; +import software.amazon.smithy.docgen.core.interceptors.OperationAuthInterceptor; +import software.amazon.smithy.docgen.core.interceptors.PaginationInterceptor; +import software.amazon.smithy.docgen.core.interceptors.PatternInterceptor; +import software.amazon.smithy.docgen.core.interceptors.RangeInterceptor; +import software.amazon.smithy.docgen.core.interceptors.RecommendedInterceptor; +import software.amazon.smithy.docgen.core.interceptors.ReferencesInterceptor; +import software.amazon.smithy.docgen.core.interceptors.RequestCompressionInterceptor; +import software.amazon.smithy.docgen.core.interceptors.RetryableInterceptor; +import software.amazon.smithy.docgen.core.interceptors.SensitiveInterceptor; +import software.amazon.smithy.docgen.core.interceptors.SinceInterceptor; +import software.amazon.smithy.docgen.core.interceptors.SparseInterceptor; +import software.amazon.smithy.docgen.core.interceptors.StreamingInterceptor; +import software.amazon.smithy.docgen.core.interceptors.TimestampFormatInterceptor; +import software.amazon.smithy.docgen.core.interceptors.UniqueItemsInterceptor; +import software.amazon.smithy.docgen.core.interceptors.UnstableInterceptor; +import software.amazon.smithy.docgen.core.interceptors.XmlAttributeInterceptor; +import software.amazon.smithy.docgen.core.interceptors.XmlFlattenedInterceptor; +import software.amazon.smithy.docgen.core.interceptors.XmlNameInterceptor; +import software.amazon.smithy.docgen.core.interceptors.XmlNamespaceInterceptor; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.MarkdownWriter; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Applies the built-in {@link DocFormat}s and base {@code CodeSection}s. + * + *

This integration runs in high priority to ensure that other integrations can see + * and react to changes it makes. To have an integration reliably run + * before this, override {@link DocIntegration#runBefore} with the output of + * {@link BuiltinsIntegration#name} in the list. Similarly, to guarantee an integration + * is run after this, override {@link DocIntegration#runAfter} with the same argument. + */ +@SmithyInternalApi +public class BuiltinsIntegration implements DocIntegration { + + @Override + public byte priority() { + // Add the builtins at a highest priority so that they almost always are run + // first. Using runBefore it is still possible to ensure an integration is run + // before this. + return 127; + } + + @Override + public List docFormats(DocSettings settings) { + return List.of( + new DocFormat("markdown", ".md", new MarkdownWriter.Factory()) + ); + } + + @Override + public List> interceptors( + DocGenerationContext context) { + // Due to the way that interceptors work, the elements at the bottom of the list will + // be called last. Since most of these append data to their sections, that means that + // the ones at the end will be at the top of the rendered pages. Therefore, interceptors + // that provide more critical information should appear at the bottom of this list. + return List.of( + new StreamingInterceptor(), + new ReferencesInterceptor(), + new MediaTypeInterceptor(), + new OperationAuthInterceptor(), + new ApiKeyAuthInterceptor(), + new TimestampFormatInterceptor(), + new JsonNameInterceptor(), + new XmlNamespaceInterceptor(), + new XmlAttributeInterceptor(), + new XmlNameInterceptor(), + new XmlFlattenedInterceptor(), + new HttpChecksumRequiredInterceptor(), + new HttpResponseCodeInterceptor(), + new HttpPayloadInterceptor(), + new HttpErrorInterceptor(), + new HttpHeaderInterceptor(), + new HttpPrefixHeadersInterceptor(), + new HttpQueryParamsInterceptor(), + new HttpQueryInterceptor(), + new HostLabelInterceptor(), + new EndpointInterceptor(), + new HttpLabelInterceptor(), + new HttpInterceptor(), + new PaginationInterceptor(), + new RequestCompressionInterceptor(), + new NoReplaceBindingInterceptor(), + new NoReplaceOperationInterceptor(), + new SparseInterceptor(), + new UniqueItemsInterceptor(), + new PatternInterceptor(), + new RangeInterceptor(), + new LengthInterceptor(), + new ExternalDocsInterceptor(), + new IdempotencyInterceptor(), + new ErrorFaultInterceptor(), + new RetryableInterceptor(), + new DefaultValueInterceptor(), + new SinceInterceptor(), + new InternalInterceptor(), + new UnstableInterceptor(), + new DeprecatedInterceptor(), + new RecommendedInterceptor(), + new NullabilityInterceptor(), + new SensitiveInterceptor() + ); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/integrations/SphinxIntegration.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/integrations/SphinxIntegration.java new file mode 100644 index 00000000000..39f63b7681f --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/integrations/SphinxIntegration.java @@ -0,0 +1,554 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.integrations; + +import static java.lang.String.format; +import static software.amazon.smithy.docgen.core.DocgenUtils.normalizeNewlines; +import static software.amazon.smithy.docgen.core.DocgenUtils.runCommand; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.docgen.core.DocFormat; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.DocIntegration; +import software.amazon.smithy.docgen.core.DocSettings; +import software.amazon.smithy.docgen.core.sections.sphinx.ConfSection; +import software.amazon.smithy.docgen.core.sections.sphinx.IndexSection; +import software.amazon.smithy.docgen.core.sections.sphinx.MakefileSection; +import software.amazon.smithy.docgen.core.sections.sphinx.RequirementsSection; +import software.amazon.smithy.docgen.core.sections.sphinx.WindowsMakeSection; +import software.amazon.smithy.docgen.core.writers.SphinxMarkdownWriter; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.node.StringNode; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.utils.IoUtils; +import software.amazon.smithy.utils.SmithyInternalApi; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * Adds Sphinx project scaffolding for compatible formats. + * + *

This integration runs in low priority to allow other integrations to generate + * files that will be picked up by sphinx-build. To have an integration reliably run + * after this, override {@link DocIntegration#runAfter} with the output of + * {@link SphinxIntegration#name} in the list. Similarly, to guarantee an integration + * is run before this, override {@link DocIntegration#runBefore} with the same argument. + * + *

To customize the project files generated by this integration, you can make use + * of {@link DocIntegration#interceptors} to intercept and modify the files before + * they're written. The following named code sections are used: + * + *

    + *
  • {@link ConfSection}: Creates the {@code conf.py} + *
  • {@link MakefileSection}: Creates the {@code Makefile} build script for unix. + *
  • {@link WindowsMakeSection}: Creates the {@code make.bat} build script for + * Windows. + *
  • {@link RequirementsSection}: Creates the {@code requirements.txt} used to + * build the docs. Any dependencies here will be installed into the environment + * used to run {@code sphinx-build}. + *
+ * + * This integration supports several customization options. To see all those options, + * see {@link SphinxSettings}. These settings are configured similarly to the doc + * generation plugin settings. Below is an example {@code smithy-build.json} with + * sphinx project auto build disabled. + * + *
{@code
+ * {
+ *     "version": "1.0",
+ *     "projections": {
+ *         "sphinx-markdown": {
+ *             "plugins": {
+ *                 "docgen": {
+ *                     "service": "com.example#DocumentedService",
+ *                     "format": "sphinx-markdown",
+ *                     "integrations": {
+ *                         "sphinx": {
+ *                             "autoBuild": false
+ *                         }
+ *                     }
+ *                 }
+ *             }
+ *         }
+ *     }
+ * }
+ * }
+ */ +@SmithyInternalApi +public final class SphinxIntegration implements DocIntegration { + private static final String MARKDOWN_FORMAT = "sphinx-markdown"; + private static final Set FORMATS = Set.of(MARKDOWN_FORMAT); + private static final Logger LOGGER = Logger.getLogger(SphinxIntegration.class.getName()); + + // The default requirements needed to build the docs. + private static final List BASE_REQUIREMENTS = parseRequirements("requirements-base.txt"); + private static final List FURO_REQUIREMENTS = parseRequirements("requirements-furo.txt"); + private static final List MARKDOWN_REQUIREMENTS = parseRequirements("requirements-markdown.txt"); + + private static final List BASE_EXTENSIONS = List.of( + "sphinx_inline_tabs", + "sphinx_copybutton", + "sphinx_design" + ); + private static final List MARKDOWN_EXTENSIONS = List.of( + "myst_parser" + ); + + private SphinxSettings settings = SphinxSettings.fromNode(Node.objectNode()); + + private static List parseRequirements(String filename) { + String requirementsFile = IoUtils.readUtf8Resource(SphinxIntegration.class, "sphinx/" + filename); + return requirementsFile.lines() + .filter(line -> !line.stripLeading().startsWith("#")) + .collect(Collectors.toList()); + } + + @Override + public String name() { + return "sphinx"; + } + + @Override + public byte priority() { + // Run at the end so that any integration-generated changes can happen. + return -128; + } + + @Override + public void configure(DocSettings settings, ObjectNode integrationSettings) { + this.settings = SphinxSettings.fromNode(integrationSettings); + } + + @Override + public List docFormats(DocSettings settings) { + return List.of( + new DocFormat(MARKDOWN_FORMAT, ".md", new SphinxMarkdownWriter.Factory()) + ); + } + + @Override + public void customize(DocGenerationContext context) { + if (!FORMATS.contains(context.docFormat().name())) { + LOGGER.finest(format( + "Format %s is not a Sphinx-compatible format, skipping Sphinx project setup.", + context.docFormat().name() + )); + return; + } + LOGGER.info("Generating Sphinx project files."); + writeIndexes(context); + writeRequirements(context); + writeConf(context); + writeMakefile(context); + runSphinx(context); + } + + private void writeRequirements(DocGenerationContext context) { + context.writerDelegator().useFileWriter("requirements.txt", writer -> { + // Merge base and configured requirements into a single immutable list + Set requirements = new LinkedHashSet<>(BASE_REQUIREMENTS); + if (context.docFormat().name().equals(MARKDOWN_FORMAT)) { + requirements.addAll(MARKDOWN_REQUIREMENTS); + } + if (settings.theme().equals("furo")) { + requirements.addAll(FURO_REQUIREMENTS); + } + requirements.addAll(settings.extraDependencies()); + writer.pushState(new RequirementsSection(context, Set.copyOf(requirements))); + requirements.forEach(writer::write); + writer.popState(); + }); + } + + private void writeConf(DocGenerationContext context) { + var service = context.model().expectShape(context.settings().service(), ServiceShape.class); + var serviceSymbol = context.symbolProvider().toSymbol(service); + + context.writerDelegator().useFileWriter("content/conf.py", writer -> { + Set extensions = new LinkedHashSet<>(BASE_EXTENSIONS); + extensions.addAll(settings.extraDependencies()); + if (context.docFormat().name().equals(MARKDOWN_FORMAT)) { + extensions.addAll(MARKDOWN_EXTENSIONS); + } + extensions = Set.copyOf(extensions); + + writer.pushState(new ConfSection(context, extensions)); + writer.putContext("extensions", extensions); + writer.putContext("isMarkdown", context.docFormat().name().equals(MARKDOWN_FORMAT)); + + writer.write(""" + # Configuration file for the Sphinx documentation builder. + # For the full list of built-in configuration values, see the documentation: + # https://www.sphinx-doc.org/en/master/usage/configuration.html + project = $1S + version = $2S + release = $2S + + extensions = [ + ${#extensions} + ${value:S}, + ${/extensions} + ] + ${?isMarkdown} + myst_enable_extensions = [ + # Makes bare links into actual links + "linkify", + + # Used to write directives that can be parsed by normal parsers + "colon_fence", + + # Used to create formatted member lists + "deflist", + ] + ${/isMarkdown} + + templates_path = ["_templates"] + html_static_path = ["_static"] + html_theme = $3S + html_title = $1S + + pygments_style = "default" + pygments_dark_style = "gruvbox-dark" + """, + serviceSymbol.getName(), + service.getVersion(), + settings.theme()); + + writer.popState(); + }); + } + + private void writeMakefile(DocGenerationContext context) { + context.writerDelegator().useFileWriter("Makefile", writer -> { + writer.pushState(new MakefileSection(context)); + writer.writeWithNoFormatting(""" + # Minimal makefile for Sphinx documentation + # You can set these variables from the command line, and also + # from the environment for the first two. + SPHINXOPTS ?= + SPHINXBUILD ?= sphinx-build + SOURCEDIR = content + BUILDDIR = build + + # Put it first so that "make" without argument is like "make help". + help: + \t@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + + .PHONY: help Makefile + + # Catch-all target: route all unknown targets to Sphinx using the new + # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). + %: Makefile + \t@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + """); + writer.popState(); + }); + + context.writerDelegator().useFileWriter("make.bat", writer -> { + writer.pushState(new WindowsMakeSection(context)); + writer.write(""" + @ECHO OFF + + pushd %~dp0 + + REM Command file for Sphinx documentation + + if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build + ) + set SOURCEDIR=content + set BUILDDIR=build + + %SPHINXBUILD% >NUL 2>NUL + if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 + ) + + if "%1" == "" goto help + + %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + goto end + + :help + %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + + :end + popd + """); + writer.popState(); + }); + } + + private void runSphinx(DocGenerationContext context) { + if (!settings.autoBuild()) { + LOGGER.info("Auto-build has been disabled. Skipping sphinx-build."); + logManualBuildInstructions(context); + return; + } + + var baseDir = context.fileManifest().getBaseDir(); + + LOGGER.info("Flushing writers in preparation for sphinx-build."); + context.writerDelegator().flushWriters(); + + // Python must be available to run sphinx + try { + LOGGER.info("Attempting to discover python3 in order to run sphinx."); + runCommand("python3 --version", baseDir); + } catch (CodegenException e) { + LOGGER.warning("Unable to find python3 on path. Skipping automatic HTML doc build."); + logManualBuildInstructions(context); + return; + } + + // TODO: detect if the user's existing python environment can be used + // You can get a big JSON document describing the python environment from + // `pip inspect` that has all the information we need. + try { + // First, we create a virtualenv to install dependencies into. This is necessary + // to not pollute the user's environment. + runCommand("python3 -m venv venv", baseDir); + + // Next, install the dependencies into the venv. + runCommand("./venv/bin/pip install -r requirements.txt", baseDir); + + // Finally, run sphinx itself. + runCommand("./venv/bin/sphinx-build -M " + settings.format() + " content build", baseDir); + + System.out.printf(normalizeNewlines(""" + Successfully built HTML docs. They can be found in "%1$s". + + Other output formats can also be built. A python virtual environment \ + has been created at "%2$s" containing the build tools needed for \ + manually building the docs in other formats. See the virtual \ + environment docs for information on how to activate it: \ + https://docs.python.org/3/library/venv.html#how-venvs-work + + Once the environment is activated, run `make %4$s` from "%3$s" to \ + to build the docs, substituting %4$s for whatever format you wish \ + to build. + + To build the docs without activating the virtual environment, simply \ + run `./venv/bin/sphinx-build -M %4$s content build` from "%3$s", \ + similarly substituting %4$s for your desired format. + + See sphinx docs for other output formats you can choose: \ + https://www.sphinx-doc.org/en/master/usage/builders/index.html + + """), + baseDir.resolve("build/" + settings.format()), + baseDir.resolve("venv"), + baseDir, + settings.format() + ); + } catch (CodegenException e) { + LOGGER.warning("Unable to automatically build HTML docs: " + e); + logManualBuildInstructions(context); + } + } + + private void logManualBuildInstructions(DocGenerationContext context) { + // TODO: try to get this printed out in the projection section + System.out.printf(normalizeNewlines(""" + To build the HTML docs manually, you need to first install the python \ + dependencies. These can be found in the `requirements.txt` file in \ + "%1$s". The easiest way to install these is by running `pip install \ + -r requirements.txt`. Depending on your environment, you may need to \ + instead install them from your system package manager, or another \ + source. + + Once the dependencies are installed, run `make %2$s` from \ + "%1$s". Other output formats can also be built. See sphinx docs for \ + other output formats: \ + https://www.sphinx-doc.org/en/master/usage/builders/index.html + + """), + context.fileManifest().getBaseDir(), + settings.format() + ); + } + + private void writeIndexes(DocGenerationContext context) { + Set paths = new HashSet<>(context.fileManifest().getFiles()); + for (var stagedFile : context.writerDelegator().getWriters().keySet()) { + paths.add(context.fileManifest().resolvePath(Paths.get(stagedFile))); + } + + Map> directories = paths.stream() + .filter(path -> path.toString().endsWith(".md") || path.toString().endsWith(".rst")) + .collect(Collectors.groupingBy(Path::getParent, Collectors.toSet())); + + for (var directory : directories.entrySet()) { + if (shouldGenerateIndex(directory.getValue())) { + writeIndex(context, directory.getKey(), directory.getValue()); + } + } + + var service = context.model().expectShape(context.settings().service(), ServiceShape.class); + var serviceSymbol = context.symbolProvider().toSymbol(service); + var serivceDirectory = Paths.get(serviceSymbol.getDefinitionFile()).getParent(); + var sourceDirectories = directories.keySet().stream() + .filter(path -> !path.equals(context.fileManifest().resolvePath(serivceDirectory))) + .map(Path::getFileName) + .map(Object::toString) + .distinct() + .sorted() + .toList(); + + if (!sourceDirectories.isEmpty()) { + context.writerDelegator().useShapeWriter(service, writer -> { + writer.putContext("sourceDirectories", sourceDirectories); + writer.write(""" + :::{toctree} + :hidden: true + + ${#sourceDirectories} + ${value:L}/index + ${/sourceDirectories} + ::: + """); + }); + } + } + + private boolean shouldGenerateIndex(Set directory) { + for (var file : directory) { + Path fileNamePath = file.getFileName(); + if (fileNamePath == null) { + continue; + } + var fileName = fileNamePath.toString(); + if (fileName.equals("index.md") || fileName.equals("index.rst")) { + return false; + } + } + return true; + } + + private void writeIndex(DocGenerationContext context, Path directory, Set contents) { + context.writerDelegator().useFileWriter(directory.resolve("index.md").toString(), writer -> { + var sourceFiles = contents.stream() + .map(Path::getFileName) + .map(Object::toString) + .distinct() + .sorted() + .toList(); + + writer.pushState(new IndexSection(context, directory, contents)); + writer.putContext("sourceFiles", sourceFiles); + writer.openHeading(StringUtils.capitalize(directory.getFileName().toString())); + writer.write(""" + :::{toctree} + ${#sourceFiles} + ${value:L} + ${/sourceFiles} + ::: + """); + writer.closeHeading(); + writer.popState(); + }); + } + + /** + * Settings for sphinx projects, regardless of their intermediate format. + * + *

These settings can be set in the {@code smithy-build.json} file under the + * {@code sphinx} key of the doc generation plugin's {@code integrations} config. + * The following example shows a {@code smithy-build.json} configuration that sets + * the default sphinx output format to be dirhtml instead of html. + * + *

{@code
+     * {
+     *     "version": "1.0",
+     *     "projections": {
+     *         "sphinx-markdown": {
+     *             "plugins": {
+     *                 "docgen": {
+     *                     "service": "com.example#DocumentedService",
+     *                     "format": "sphinx-markdown",
+     *                     "integrations": {
+     *                         "sphinx": {
+     *                             "format": "dirhtml"
+     *                         }
+     *                     }
+     *                 }
+     *             }
+     *         }
+     *     }
+     * }
+     * }
+ * + * @param format The sphinx output format that will be built automatically during + * generation. The default is html. See + * + * sphinx docs for other output format options. + * @param theme The sphinx html theme to use. The default is alabaster. If your + * chosen theme requires a python dependency to be added, use the + * {@link #extraDependencies} setting. + * @param extraDependencies Any extra python dependencies that should be added to + * the {@code requirements.txt} file for the sphinx project. + * These can be particularly useful for custom {@link #theme}s. + * @param extraExtensions Any extra sphinx extensions that should be added to the + * {@code conf.py} file for the sphinx project. + * @param autoBuild Whether to automatically attempt to build the generated sphinx + * project. The default is true. This will attempt to discover Python + * 3 on the path, create a virtual environment inside the output + * directory, install all the dependencies into that virtual environment, + * and finally run sphinx-build. + */ + @SmithyUnstableApi + public record SphinxSettings( + String format, + String theme, + List extraDependencies, + List extraExtensions, + boolean autoBuild + ) { + /** + * Load the settings from an {@code ObjectNode}. + * + * @param node the {@code ObjectNode} to load settings from. + * @return loaded settings based on the given node. + */ + public static SphinxSettings fromNode(ObjectNode node) { + List extraDependencies = List.of(); + if (node.containsMember("extraDependencies")) { + extraDependencies = node.expectArrayMember("extraDependencies") + .getElementsAs(StringNode::getValue); + } + List extraExtensions = List.of(); + if (node.containsMember("extraExtensions")) { + extraExtensions = node.expectArrayMember("extraExtensions") + .getElementsAs(StringNode::getValue); + } + return new SphinxSettings( + node.getStringMemberOrDefault("format", "html"), + node.getStringMemberOrDefault("theme", "furo"), + extraDependencies, + extraExtensions, + node.getBooleanMemberOrDefault("autoBuild", true) + ); + } + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ApiKeyAuthInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ApiKeyAuthInterceptor.java new file mode 100644 index 00000000000..d0c39a61a39 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ApiKeyAuthInterceptor.java @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait; +import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait.Location; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds additional context to the description of api key auth based on the customized values. + */ +@SmithyInternalApi +public final class ApiKeyAuthInterceptor implements CodeInterceptor { + private static final Pair AUTH_HEADER_REF = Pair.of( + "Authorization header", "https://datatracker.ietf.org/doc/html/rfc9110.html#section-11.4" + ); + + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + public boolean isIntercepted(ShapeDetailsSection section) { + return section.shape().getId().equals(HttpApiKeyAuthTrait.ID); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeDetailsSection section) { + var service = section.context().model().expectShape(section.context().settings().service()); + var trait = service.expectTrait(HttpApiKeyAuthTrait.class); + writer.putContext("name", trait.getName()); + writer.putContext("location", trait.getIn().equals(Location.HEADER) ? "header" : "query string"); + writer.putContext("scheme", trait.getScheme()); + writer.putContext("authHeader", AUTH_HEADER_REF); + writer.write(""" + The API key must be bound to the ${location:L} using the key ${name:`}.${?scheme} \ + Additionally, the scheme used in the ${authHeader:R} must be ${scheme:`}.${/scheme} + + $L""", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/DefaultValueInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/DefaultValueInterceptor.java new file mode 100644 index 00000000000..39bb8e45e62 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/DefaultValueInterceptor.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.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.traits.DefaultTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds default value information to trait documentation. + */ +@SmithyInternalApi +public final class DefaultValueInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), DefaultTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + var defaultValue = section.shape().getMemberTrait(section.context().model(), DefaultTrait.class).get().toNode(); + writer.write(""" + $B $` + + $L""", "Default Value:", Node.printJson(defaultValue), previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/DeprecatedInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/DeprecatedInterceptor.java new file mode 100644 index 00000000000..f53d6ff694e --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/DeprecatedInterceptor.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.DeprecatedTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds deprecation warnings to shape docs. + */ +@SmithyInternalApi +public final class DeprecatedInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), DeprecatedTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + var trait = section.shape().getMemberTrait(section.context().model(), DeprecatedTrait.class).get(); + writer.putContext("since", trait.getSince()); + writer.openAdmonition(NoticeType.WARNING, w -> { + w.write("Deprecated${?since} since ${since:L}${/since}"); + }); + writer.putContext("message", trait.getMessage()); + writer.write(""" + ${?message}${message:L}${/message} + ${^message}This has been deprecated${?since} since version ${since:L}${/since}.${/message} + """); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/EndpointInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/EndpointInterceptor.java new file mode 100644 index 00000000000..28c90750429 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/EndpointInterceptor.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.EndpointTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds endpoint prefix information to operations based on the + * + * endpoint trait if the protocol supports it. + */ +@SmithyInternalApi +public final class EndpointInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return EndpointTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return EndpointTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, EndpointTrait trait) { + writer.putContext("hasLabels", !trait.getHostPrefix().getLabels().isEmpty()); + writer.write(""" + $B $` + ${?hasLabels} + + To resolve the endpoint prefix, replace any portions surrounded with braces with the \ + URI-escaped value of the corresponding member. + ${/hasLabels} + + $L""", "Host prefix:", trait.getHostPrefix(), previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ErrorFaultInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ErrorFaultInterceptor.java new file mode 100644 index 00000000000..e796c193fa7 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ErrorFaultInterceptor.java @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.traits.ErrorTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a line to the error shape docs to indicate whether the error is a client or + * service error. + */ +@SmithyInternalApi +public final class ErrorFaultInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().hasTrait(ErrorTrait.class); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + var fault = section.shape().expectTrait(ErrorTrait.class).getValue(); + writer.write(""" + This is an error caused by the $L. + + $L + """, fault, previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ExternalDocsInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ExternalDocsInterceptor.java new file mode 100644 index 00000000000..ff8b3bf4fa2 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ExternalDocsInterceptor.java @@ -0,0 +1,41 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.ExternalDocumentationTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.Pair; + +/** + * Adds external doc links after a shape's modeled docs based on the + * + * externalDocumentation trait. + */ +public final class ExternalDocsInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + public boolean isIntercepted(ShapeDetailsSection section) { + return section.shape().getMemberTrait(section.context().model(), ExternalDocumentationTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeDetailsSection section) { + var trait = section.shape().getMemberTrait(section.context().model(), ExternalDocumentationTrait.class).get(); + writer.openAdmonition(NoticeType.INFO); + trait.getUrls().entrySet().stream() + .map(entry -> Pair.of(entry.getKey(), entry.getValue())) + .forEach(pair -> writer.write("$R\n", pair)); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HostLabelInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HostLabelInterceptor.java new file mode 100644 index 00000000000..f35ec33bf69 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HostLabelInterceptor.java @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HostLabelTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Documents usage for members targeted with the + * + * hostLabel trait if the protocol supports it.. + */ +@SmithyInternalApi +public final class HostLabelInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HostLabelTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HostLabelTrait.ID; + } + + @Override + public boolean isIntercepted(ProtocolSection section) { + // It's possible to use this trait somewhere where it has no meaning, but we don't + // want to document in those cases. + var index = OperationIndex.of(section.context().model()); + return index.isInputStructure(section.shape().getId().withoutMember()) && super.isIntercepted(section); + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HostLabelTrait trait) { + var segment = "{" + section.shape().getId().getName() + "}"; + writer.write(""" + This is additionally bound to the host prefix. Its value should be URI-escaped and \ + and inserted in place of the $` segment. It must also be serialized to its normal \ + binding location. + + $L""", segment, previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpChecksumRequiredInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpChecksumRequiredInterceptor.java new file mode 100644 index 00000000000..36b22a07a69 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpChecksumRequiredInterceptor.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information about query bindings from the + * + * httpQuery trait if the protocol supports it. + */ +@SmithyInternalApi +public final class HttpChecksumRequiredInterceptor extends ProtocolTraitInterceptor { + private static final Pair CONTENT_MD5 = Pair.of( + "Content-MD5", "https://datatracker.ietf.org/doc/html/rfc1864.html" + ); + + @Override + protected Class getTraitClass() { + return HttpChecksumRequiredTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpChecksumRequiredTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpChecksumRequiredTrait trait) { + writer.writeWithNoFormatting(previousText + "\n"); + writer.openAdmonition(NoticeType.IMPORTANT); + writer.write("This operation REQUIRES a checksum, such as $R.", CONTENT_MD5); + writer.closeAdmonition(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpErrorInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpErrorInterceptor.java new file mode 100644 index 00000000000..54d9085610c --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpErrorInterceptor.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpErrorTrait; + +/** + * Adds the http response code for errors from the + * + * httpError trait if the protocol supports it. + */ +public final class HttpErrorInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HttpErrorTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpErrorTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpErrorTrait trait) { + writer.putContext("code", trait.getCode()); + writer.write(""" + $B ${code:`} + + $L""", "HTTP Error Code:", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpHeaderInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpHeaderInterceptor.java new file mode 100644 index 00000000000..be75969e1f8 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpHeaderInterceptor.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpHeaderTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information about header bindings from the + * + * httpHeader trait if the protocol supports it. + */ +@SmithyInternalApi +public final class HttpHeaderInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HttpHeaderTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpHeaderTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpHeaderTrait trait) { + var target = section.context().model().expectShape(section.shape().asMemberShape().get().getTarget()); + writer.putContext("key", trait.getValue()); + writer.putContext("list", target.isListShape()); + writer.write(""" + This is bound to the HTTP header ${param:`}.${?list} Each element in \ + the list should be sent as its own header using the same key for each \ + value. The list may instead be concatenated with commas separating each \ + value.${/list} + + $L""", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpInterceptor.java new file mode 100644 index 00000000000..3db54f0faaf --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpInterceptor.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.pattern.SmithyPattern.Segment; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information to operations from the + * + * http trait. + */ +@SmithyInternalApi +public final class HttpInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HttpTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpTrait trait) { + writer.putContext("hasLabels", !trait.getUri().getLabels().isEmpty()); + writer.putContext("greedyLabel", + trait.getUri().getGreedyLabel().map(Segment::getContent)); + writer.write(""" + $B $` + + $B $` + ${?hasLabels} + + To resolve the path segment of the URI, replace any segments surrounded with + braces with the URI-escaped value of the corresponding member.${?greedyLabel} \ + When escaping the value of the ${greedyLabel:`} segment, do not escape any \ + backslashes ($`).${/greedyLabel} + ${/hasLabels} + + $L""", "HTTP Method:", trait.getMethod(), "URI:", trait.getUri(), "/", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpLabelInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpLabelInterceptor.java new file mode 100644 index 00000000000..29836ad2fe2 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpLabelInterceptor.java @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpLabelTrait; +import software.amazon.smithy.model.traits.HttpTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information about label bindings from the + * + * httpLabel trait if the protocol supports it. + */ +@SmithyInternalApi +public final class HttpLabelInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HttpLabelTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpLabelTrait.ID; + } + + @Override + public boolean isIntercepted(ProtocolSection section) { + // It's possible to use this trait somewhere where it has no meaning, but we don't + // want to document in those cases. + var index = OperationIndex.of(section.context().model()); + return index.isInputStructure(section.shape().getId().withoutMember()) && super.isIntercepted(section); + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpLabelTrait trait) { + var index = OperationIndex.of(section.context().model()); + writer.putContext("greedy", index.getInputBindings(section.shape()).stream().findFirst() + .map(operation -> operation.expectTrait(HttpTrait.class)) + .flatMap(httpTrait -> httpTrait.getUri().getGreedyLabel()) + .map(segment -> segment.getContent().equals(section.shape().getId().getName())) + .orElse(false)); + var segment = "{" + section.shape().getId().getName() + "}"; + writer.write(""" + This is bound to the path of the URI. Its value should be URI-escaped and \ + and inserted in place of the $` segment.\ + ${?greedy} + When escaping this value, do not escape any backslashes ($`). + ${/greedy} + + $L""", segment, "/", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpPayloadInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpPayloadInterceptor.java new file mode 100644 index 00000000000..31424c2ec77 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpPayloadInterceptor.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpPayloadTrait; +import software.amazon.smithy.model.traits.RequiresLengthTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information about payload bindings from the + * + * httpPayload trait if the protocol supports it. + */ +@SmithyInternalApi +public final class HttpPayloadInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HttpPayloadTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpPayloadTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpPayloadTrait trait) { + var target = section.context().model().expectShape(section.shape().asMemberShape().get().getTarget()); + writer.pushState(); + writer.putContext("requiresLength", target.hasTrait(RequiresLengthTrait.class)); + writer.write(""" + This is bound directly to the HTTP message body without wrapping.${?requiresLength} \ + Its size must be sent as the value of the $` header.${/requiresLength} + + $L""", "Content-Length", previousText); + writer.popState(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpPrefixHeadersInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpPrefixHeadersInterceptor.java new file mode 100644 index 00000000000..636682048ce --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpPrefixHeadersInterceptor.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpPrefixHeadersTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information about header bindings from the + * + * httpPrefixHeaders trait if the protocol supports it. + */ +@SmithyInternalApi +public final class HttpPrefixHeadersInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HttpPrefixHeadersTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpPrefixHeadersTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpPrefixHeadersTrait trait) { + writer.putContext("prefix", trait.getValue()); + writer.write(""" + Each pair in this map represents an HTTP header${?prefix} with the prefix \ + ${prefix:`}${/prefix}. + + $L""", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpQueryInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpQueryInterceptor.java new file mode 100644 index 00000000000..3e0589cd1a3 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpQueryInterceptor.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.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpQueryTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information about query bindings from the + * + * httpQuery trait if the protocol supports it. + */ +@SmithyInternalApi +public final class HttpQueryInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HttpQueryTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpQueryTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpQueryTrait trait) { + var target = section.context().model().expectShape(section.shape().asMemberShape().get().getTarget()); + writer.putContext("param", trait.getValue()); + writer.putContext("list", target.isListShape()); + writer.write(""" + This is bound to the HTTP query parameter ${param:`}.${?list} Each element in \ + the list is represented by its own key-value pair, each instance using the \ + same key.${/list} + + $L""", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpQueryParamsInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpQueryParamsInterceptor.java new file mode 100644 index 00000000000..e5b7e279416 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpQueryParamsInterceptor.java @@ -0,0 +1,45 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.MapShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpQueryParamsTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information about query bindings from the + * + * httpQueryParams trait if the protocol supports it. + */ +@SmithyInternalApi +public final class HttpQueryParamsInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HttpQueryParamsTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpQueryParamsTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpQueryParamsTrait trait) { + var memberTarget = section.shape().asMemberShape().get().getTarget(); + var map = section.context().model().expectShape(memberTarget, MapShape.class); + var valueTarget = section.context().model().expectShape(map.getValue().getTarget()); + writer.putContext("list", valueTarget.isListShape()); + writer.write(""" + Each pair in this map represents an HTTP query parameter.${?list} Each element in \ + the value lists is represented by its own key-value pair, each instance using the \ + same key.${/list} + + $L""", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpResponseCodeInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpResponseCodeInterceptor.java new file mode 100644 index 00000000000..19b414858ce --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/HttpResponseCodeInterceptor.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.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.HttpResponseCodeTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information about response code bindings from the + * + * httpResponseCode trait if the protocol supports it. + */ +@SmithyInternalApi +public final class HttpResponseCodeInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return HttpResponseCodeTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return HttpResponseCodeTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, HttpResponseCodeTrait trait) { + writer.write(""" + This value represents the HTTP response code for the operation invocation. + + $L""", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/IdempotencyInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/IdempotencyInterceptor.java new file mode 100644 index 00000000000..6e45c776970 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/IdempotencyInterceptor.java @@ -0,0 +1,106 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import java.util.Optional; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.knowledge.OperationIndex; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.traits.IdempotencyTokenTrait; +import software.amazon.smithy.model.traits.IdempotentTrait; +import software.amazon.smithy.model.traits.ReadonlyTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Provides information about idempotency depending on a number of traits. + */ +@SmithyInternalApi +public final class IdempotencyInterceptor implements CodeInterceptor { + private static final Pair IDEMPOTENT_REF = Pair.of( + "idempotent", "https://datatracker.ietf.org/doc/html/rfc7231.html#section-4.2.2" + ); + private static final Pair UUID_REF = Pair.of( + "UUID", "https://tools.ietf.org/html/rfc4122.html" + ); + + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + public boolean isIntercepted(ShapeDetailsSection section) { + var shape = section.shape(); + var model = section.context().model(); + var operationIndex = OperationIndex.of(model); + + if (shape.hasTrait(IdempotencyTokenTrait.class) + && operationIndex.isInputStructure(shape.asMemberShape().get().getContainer())) { + return true; + } + + var target = shape.isMemberShape() + ? model.expectShape(shape.asMemberShape().get().getTarget()) + : shape; + + if (!target.isOperationShape()) { + return false; + } + + return shape.getMemberTrait(model, IdempotentTrait.class).isPresent() + || shape.getMemberTrait(model, ReadonlyTrait.class).isPresent() + || getIdempotencyToken(model, target.asOperationShape().get()).isPresent(); + } + + private Optional getIdempotencyToken(Model model, OperationShape operation) { + var input = model.expectShape(operation.getInputShape()); + for (var member : input.members()) { + if (member.hasTrait(IdempotencyTokenTrait.class)) { + return Optional.of(member); + } + } + return Optional.empty(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeDetailsSection section) { + if (section.shape().isMemberShape()) { + writer.openAdmonition(NoticeType.NOTE); + writer.write(""" + This value will be used by the service to ensure the request is $R. \ + Clients SHOULD automatically populate this (typically with a $R) if \ + it was not explicitly set. + + """, IDEMPOTENT_REF, UUID_REF); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + return; + } + + var operation = section.shape().asOperationShape().get(); + var idempotencyToken = getIdempotencyToken(section.context().model(), operation) + .map(member -> section.context().symbolProvider().toSymbol(member)) + .map(symbol -> SymbolReference.builder() + .alias(String.format("idempotency token (%s)", symbol.getName())) + .symbol(symbol) + .build()); + writer.putContext("token", idempotencyToken); + writer.openAdmonition(NoticeType.NOTE); + writer.write(""" + This operation is $R${?token} when the ${token:R} is set${/token}. + + """, IDEMPOTENT_REF); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/InternalInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/InternalInterceptor.java new file mode 100644 index 00000000000..0c525863555 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/InternalInterceptor.java @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import java.util.logging.Logger; +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.InternalTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds an admonition to shapes marked as + * + * internal. + */ +@SmithyInternalApi +public final class InternalInterceptor implements CodeInterceptor { + private static final Logger LOGGER = Logger.getLogger(InternalInterceptor.class.getName()); + + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), InternalTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + // TODO: add a DANGER-level validator + LOGGER.warning(String.format(""" + Internal shape %s found. Adding DANGER admonition to its documentation. \ + If this isn't meant to be documented, use a trait filter in your projection \ + to filter out internal shapes: \ + https://smithy.io/2.0/guides/building-models/build-config.html#excludeshapesbytrait""", + section.shape().getId())); + writer.openAdmonition(NoticeType.DANGER); + writer.write("This is part of the internal API not available to external customers."); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/JsonNameInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/JsonNameInterceptor.java new file mode 100644 index 00000000000..34111c009a3 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/JsonNameInterceptor.java @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.JsonNameTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a member's + * jsonName to the {@link ProtocolSection} if the protocol supports it. + */ +@SmithyInternalApi +public final class JsonNameInterceptor extends ProtocolTraitInterceptor { + + @Override + protected Class getTraitClass() { + return JsonNameTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return JsonNameTrait.ID; + } + + @Override + public void write(DocWriter writer, String previousText, ProtocolSection section, JsonNameTrait trait) { + writer.putContext("jsonKeyName", "JSON key name:"); + writer.write(""" + ${jsonKeyName:B} $` + + $L""", trait.getValue(), previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/LengthInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/LengthInterceptor.java new file mode 100644 index 00000000000..4498bcf1201 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/LengthInterceptor.java @@ -0,0 +1,71 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.LengthTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information to shapes if they have the + * + * length trait. + */ +@SmithyInternalApi +public final class LengthInterceptor implements CodeInterceptor { + private static final Pair UNICODE_SCALAR_VALUE_REFERENCE = Pair.of( + "Unicode scalar values", "https://www.unicode.org/glossary/#unicode_scalar_value" + ); + + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), LengthTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + var trait = section.shape().getMemberTrait(section.context().model(), LengthTrait.class).get(); + writer.putContext("min", trait.getMin()); + writer.putContext("max", trait.getMax()); + + var target = section.shape().isMemberShape() + ? section.context().model().expectShape(section.shape().asMemberShape().get().getTarget()) + : section.shape(); + + writer.write(""" + ${?min} + $1B ${min:L} $3C. + + ${/min} + ${?max} + $2B ${max:L} $3C. + + ${/max} + $4L""", + "Minimum length:", + "Maximum length:", + writer.consumer(w -> writeUnit(w, target)), + previousText); + } + + private void writeUnit(DocWriter writer, Shape target) { + switch (target.getType()) { + case MAP -> writer.writeInline("pairs"); + case STRING -> writer.writeInline("$R", UNICODE_SCALAR_VALUE_REFERENCE); + case BLOB -> writer.writeInline("bytes"); + default -> writer.writeInline("elements"); + } + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/MediaTypeInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/MediaTypeInterceptor.java new file mode 100644 index 00000000000..129dc841724 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/MediaTypeInterceptor.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.traits.MediaTypeTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +// TODO: Add content type to operation docs. Need a way to determine http protocols first. +/** + * Adds the media type to member documentation if it has the + * + * mediaType trait. + */ +@SmithyInternalApi +public final class MediaTypeInterceptor implements CodeInterceptor.Prepender { + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), MediaTypeTrait.class).isPresent(); + } + + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public void prepend(DocWriter writer, ShapeSubheadingSection section) { + var trait = section.shape().getMemberTrait(section.context().model(), MediaTypeTrait.class).get(); + writer.write(""" + $B $` + + """, "Media Type:", trait.getValue()); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NoReplaceBindingInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NoReplaceBindingInterceptor.java new file mode 100644 index 00000000000..e124e2fb2a4 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NoReplaceBindingInterceptor.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.sections.LifecycleOperationSection; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds the noReplace admonition to the resource lifecycle property list on the resource page. + */ +@SmithyInternalApi +public final class NoReplaceBindingInterceptor extends NoReplaceInterceptor { + @Override + public Class sectionType() { + return LifecycleOperationSection.class; + } + + @Override + Shape getShape(LifecycleOperationSection section) { + return section.operation(); + } + + @Override + DocGenerationContext getContext(LifecycleOperationSection section) { + return section.context(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NoReplaceInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NoReplaceInterceptor.java new file mode 100644 index 00000000000..97bd8dba644 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NoReplaceInterceptor.java @@ -0,0 +1,83 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import java.util.Optional; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.knowledge.BottomUpIndex; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.NoReplaceTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a note that a resource's put operation can't do updates if it has the + * + * noReplace trait. + */ +@SmithyInternalApi +abstract class NoReplaceInterceptor implements CodeInterceptor { + @Override + public boolean isIntercepted(S section) { + var shape = getShape(section); + var resource = getResource(getContext(section), shape); + return resource.isPresent() + && resource.get().hasTrait(NoReplaceTrait.class) + && resource.get().getPut().map(put -> put.equals(shape.getId())).orElse(false); + } + + @Override + public void write( + DocWriter writer, + String previousText, + S section + ) { + var context = getContext(section); + var resource = getResource(context, getShape(section)).get(); + var resourceReference = SymbolReference.builder() + .alias("resource") + .symbol(context.symbolProvider().toSymbol(resource)) + .build(); + var updateSymbolReference = resource.getUpdate() + .map(update -> context.model().expectShape(update)) + .map(update -> context.symbolProvider().toSymbol(update)) + .map(symbol -> SymbolReference.builder().alias("update lifecycle operation").symbol(symbol).build()); + writer.putContext("update", updateSymbolReference); + writer.writeWithNoFormatting(previousText); + writer.openAdmonition(NoticeType.NOTE); + writer.write(""" + This operation cannot be used to update the $1R.\ + ${?update} To update the $1R, use the ${update:R}.${/update}""", + resourceReference); + writer.closeAdmonition(); + } + + /** + * Extracts the shape for the section. + * @param section the section to extract the shape from. + * @return returns the section's shape. + */ + abstract Shape getShape(S section); + + + /** + * Extracts the context for the section. + * @param section the section to extract the context from. + * @return returns the section's context. + */ + abstract DocGenerationContext getContext(S section); + + private Optional getResource(DocGenerationContext context, Shape shape) { + var bottomUpIndex = BottomUpIndex.of(context.model()); + return bottomUpIndex.getResourceBinding(context.settings().service(), shape); + } + +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NoReplaceOperationInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NoReplaceOperationInterceptor.java new file mode 100644 index 00000000000..2b987601b76 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NoReplaceOperationInterceptor.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds the noReplace admonition to the resource operation's doc page. + */ +@SmithyInternalApi +public final class NoReplaceOperationInterceptor extends NoReplaceInterceptor { + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + Shape getShape(ShapeDetailsSection section) { + return section.shape(); + } + + @Override + DocGenerationContext getContext(ShapeDetailsSection section) { + return section.context(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NullabilityInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NullabilityInterceptor.java new file mode 100644 index 00000000000..a9a068a8ee0 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/NullabilityInterceptor.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.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.knowledge.NullableIndex; +import software.amazon.smithy.model.knowledge.NullableIndex.CheckMode; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds nullability information to member docs. + */ +@SmithyInternalApi +public final class NullabilityInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + if (!section.shape().isMemberShape()) { + return false; + } + + // It might seem crazy to create this for *every member*, but knowledge indexes + // actually get cached on the model so in reality it's only created once. + var index = NullableIndex.of(section.context().model()); + return !index.isMemberNullable(section.shape().asMemberShape().get(), CheckMode.SERVER); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + writer.writeBadge(NoticeType.WARNING, "REQUIRED") + .write("\n\n$L", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/OperationAuthInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/OperationAuthInterceptor.java new file mode 100644 index 00000000000..cda1539a230 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/OperationAuthInterceptor.java @@ -0,0 +1,99 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import java.util.List; +import software.amazon.smithy.docgen.core.DocgenUtils; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.knowledge.ServiceIndex; +import software.amazon.smithy.model.knowledge.ServiceIndex.AuthSchemeMode; +import software.amazon.smithy.model.shapes.ToShapeId; +import software.amazon.smithy.model.traits.synthetic.NoAuthTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a priority list of supported auth schemes for operations with optional auth or + * operations which don't support all of a service's auth schemes. + */ +@SmithyInternalApi +public final class OperationAuthInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + public boolean isIntercepted(ShapeDetailsSection section) { + if (!section.shape().isOperationShape()) { + return false; + } + var index = ServiceIndex.of(section.context().model()); + var service = section.context().settings().service(); + + // Only add the admonition if the service has auth in the first place. + var serviceAuth = index.getAuthSchemes(service); + if (serviceAuth.isEmpty()) { + return false; + } + + // Only add the admonition if the operations' effective auth schemes differs + // from the total list of available auth schemes on the service. + var operationAuth = index.getEffectiveAuthSchemes(service, section.shape(), AuthSchemeMode.NO_AUTH_AWARE); + return !operationAuth.keySet().equals(serviceAuth.keySet()); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeDetailsSection section) { + writer.writeWithNoFormatting(previousText); + writer.openAdmonition(NoticeType.IMPORTANT); + + var index = ServiceIndex.of(section.context().model()); + var service = section.context().settings().service(); + var operation = section.shape(); + + + var serviceAuth = DocgenUtils.getPrioritizedServiceAuth(section.context().model(), service); + var operationAuth = List.copyOf( + index.getEffectiveAuthSchemes(service, operation, AuthSchemeMode.MODELED).keySet()); + + if (serviceAuth.equals(operationAuth)) { + // If the total service auth and effective *modeled* operation auth are the same, + // that means that the operation just has optional auth since isIntercepted would + // return false otherwise. It would have been overly confusing to include this + // case in the big text block below. + writer.write(""" + This operation may be optionally called without authentication. + """); + writer.closeAdmonition(); + return; + } + + var operationSchemes = operationAuth.stream() + .map(id -> section.context().symbolProvider().toSymbol(section.context().model().expectShape(id))) + .toList(); + + writer.putContext("optional", supportsNoAuth(index, service, section.shape())); + writer.putContext("schemes", operationSchemes); + writer.putContext("multipleSchemes", operationSchemes.size() > 1); + + writer.write(""" + ${?schemes}This operation ${?optional}may optionally${/optional}${^optional}MUST${/optional} \ + be called with ${?multipleSchemes}one of the following priority-ordered auth schemes${/multipleSchemes}\ + ${^multipleSchemes}the following auth scheme${/multipleSchemes}: \ + ${#schemes}${value:R}${^key.last}, ${/key.last}${/schemes}.${/schemes}\ + ${^schemes}${?optional}This operation must be called without authentication.${/optional}${/schemes} + """); + writer.closeAdmonition(); + } + + private boolean supportsNoAuth(ServiceIndex index, ToShapeId service, ToShapeId operation) { + return index.getEffectiveAuthSchemes(service, operation, AuthSchemeMode.NO_AUTH_AWARE) + .containsKey(NoAuthTrait.ID); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/PaginationInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/PaginationInterceptor.java new file mode 100644 index 00000000000..656fd33f30a --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/PaginationInterceptor.java @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.knowledge.PaginatedIndex; +import software.amazon.smithy.model.traits.PaginatedTrait; +import software.amazon.smithy.utils.CodeInterceptor; + +/** + * This adds pagination information to operation docs. + */ +public final class PaginationInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + public boolean isIntercepted(ShapeDetailsSection section) { + return section.shape().isOperationShape() && section.shape().hasTrait(PaginatedTrait.class); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeDetailsSection section) { + var paginatedIndex = PaginatedIndex.of(section.context().model()); + var service = section.context().settings().service(); + var paginationInfo = paginatedIndex.getPaginationInfo(service, section.shape()).get(); + var symbolProvider = section.context().symbolProvider(); + writer.putContext("size", paginationInfo.getPageSizeMember().map(symbolProvider::toSymbol)); + writer.putContext("inputToken", SymbolReference.builder() + .symbol(symbolProvider.toSymbol(paginationInfo.getInputTokenMember())) + .alias("input token") + .build()); + + var outputTokenPath = paginationInfo.getOutputTokenMemberPath(); + var outputToken = outputTokenPath.get(outputTokenPath.size() - 1); + writer.putContext("outputToken", SymbolReference.builder() + .symbol(symbolProvider.toSymbol(outputToken)) + .alias("output token") + .build()); + + writer.openAdmonition(NoticeType.IMPORTANT); + writer.write(""" + This operation returns partial results in pages${?size}, whose maximum size may be + configured with ${size:R}${/size}. Each request may return an ${outputToken:R} that \ + may be used as an ${inputToken:R} in subsequent requests to fetch the next page of results. \ + If the operation does not return an ${outputToken:R}, that means that there are \ + no more results. If the operation returns a repeated ${outputToken:R}, there MAY be \ + more results later."""); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/PatternInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/PatternInterceptor.java new file mode 100644 index 00000000000..f26dfc90a84 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/PatternInterceptor.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.traits.PatternTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information to shapes if they have the + * + * pattern trait. + */ +@SmithyInternalApi +public final class PatternInterceptor implements CodeInterceptor { + private static final Pair REGEX_REF = Pair.of( + "ECMA 262 regular expression", "https://262.ecma-international.org/8.0/#sec-patterns" + ); + + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), PatternTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + var trait = section.shape().getMemberTrait(section.context().model(), PatternTrait.class).get(); + writer.write(""" + This value must match the following $R: $` + + $L""", REGEX_REF, trait.getValue(), previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ProtocolTraitInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ProtocolTraitInterceptor.java new file mode 100644 index 00000000000..a2a10a97aa7 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ProtocolTraitInterceptor.java @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.ProtocolDefinitionTrait; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.utils.CodeInterceptor; + +/** + * Implements an interceptor that adds protocol trait documentation. + * + * @param The class of the protocol trait. + */ +abstract class ProtocolTraitInterceptor implements CodeInterceptor { + + /** + * @return returns the class of the protocol trait. + */ + protected abstract Class getTraitClass(); + + /** + * @return returns the shape id of the protocol trait. + */ + protected abstract ShapeId getTraitId(); + + @Override + public boolean isIntercepted(ProtocolSection section) { + if (section.shape().getMemberTrait(section.context().model(), getTraitClass()).isEmpty()) { + return false; + } + var protocolShape = section.context().model().expectShape(section.protocol()); + var protocolDefinition = protocolShape.expectTrait(ProtocolDefinitionTrait.class); + return protocolDefinition.getTraits().contains(getTraitId()); + } + + @Override + public Class sectionType() { + return ProtocolSection.class; + } + + @Override + public void write(DocWriter writer, String previousText, ProtocolSection section) { + var trait = section.shape().getMemberTrait(section.context().model(), getTraitClass()).get(); + write(writer, previousText, section, trait); + } + + abstract void write(DocWriter writer, String previousText, ProtocolSection section, T trait); +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RangeInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RangeInterceptor.java new file mode 100644 index 00000000000..6cb25f2f572 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RangeInterceptor.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.traits.RangeTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information to shapes if they have the + * + * range trait. + */ +@SmithyInternalApi +public final class RangeInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), RangeTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + var trait = section.shape().getMemberTrait(section.context().model(), RangeTrait.class).get(); + writer.putContext("min", trait.getMin()); + writer.putContext("max", trait.getMax()); + + writer.write(""" + ${?min} + $1B ${min:L} + + ${/min} + ${?max} + $2B ${max:L} + + ${/max} + $3L""", + "Minimum:", + "Maximum:", + previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RecommendedInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RecommendedInterceptor.java new file mode 100644 index 00000000000..087f7a7ed8d --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RecommendedInterceptor.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.RecommendedTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a notice for recommended members based on the + * + * recommended trait. + */ +@SmithyInternalApi +public final class RecommendedInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().hasTrait(RecommendedTrait.class); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + var trait = section.shape().expectTrait(RecommendedTrait.class); + writer.putContext("reason", trait.getReason()); + writer.writeBadge(NoticeType.IMPORTANT, "RECOMMENDED"); + writer.write(""" + ${?reason} ${reason:L}${/reason} + + $L""", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ReferencesInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ReferencesInterceptor.java new file mode 100644 index 00000000000..f034663bd3a --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/ReferencesInterceptor.java @@ -0,0 +1,131 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.ReferencesTrait; +import software.amazon.smithy.model.traits.ReferencesTrait.Reference; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a "see also" to structures / operations that reference resources using + * the references trait. + */ +@SmithyInternalApi +public final class ReferencesInterceptor implements CodeInterceptor.Appender { + private static final Logger LOGGER = Logger.getLogger(ReferencesInterceptor.class.getName()); + + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + public boolean isIntercepted(ShapeDetailsSection section) { + var model = section.context().model(); + if (model.getResourceShapes().isEmpty() && section.context().settings().references().isEmpty()) { + // If there's nothing referenceable, we can return quickly. + return false; + } + + if (section.shape().isMemberShape()) { + // Since the containing shape will show information about the reference, it's not + // necessary to also show that on the members. + return false; + } + + return !getLocalReferences(section.context(), section.shape()).isEmpty(); + } + + @Override + public void append(DocWriter writer, ShapeDetailsSection section) { + var model = section.context().model(); + var symbolProvider = section.context().symbolProvider(); + var localRefs = getLocalReferences(section.context(), section.shape()); + var externalRefs = section.context().settings().references(); + var serviceResources = TopDownIndex.of(model).getContainedResources(section.context().settings().service()) + .stream() + .map(Shape::getId) + .collect(Collectors.toSet()); + + // This is a mapping of reference link to optional rel type. If `rel` isn't set, + // it'll be an empty optional that won't get displayed. + var references = new LinkedHashMap<>(localRefs.size()); + for (var reference : localRefs) { + if (serviceResources.contains(reference.getResource())) { + var symbol = symbolProvider.toSymbol(model.expectShape(reference.getResource())); + references.put(symbol, reference.getRel()); + } else if (externalRefs.containsKey(reference.getResource())) { + var ref = Pair.of(reference.getResource().getName(), externalRefs.get(reference.getResource())); + references.put(ref, reference.getRel()); + } + } + + writer.pushState(); + writer.putContext("refs", references); + writer.putContext("multipleRefs", references.size() > 1); + writer.openAdmonition(NoticeType.INFO); + writer.write(""" + This references \ + ${?multipleRefs}the following resources: ${/multipleRefs}\ + ${^multipleRefs}the resource ${/multipleRefs}\ + ${#refs} + ${key:R}${?value} (rel type: ${value:`})${/value}${^key.last}, ${/key.last}\ + ${/refs} + . + """); + writer.closeAdmonition(); + writer.popState(); + } + + private Set getLocalReferences(DocGenerationContext context, Shape shape) { + var model = context.model(); + var references = new LinkedHashSet(); + if (shape.isOperationShape()) { + var operation = shape.asOperationShape().get(); + references.addAll(getLocalReferences(context, model.expectShape(operation.getInputShape()))); + references.addAll(getLocalReferences(context, model.expectShape(operation.getInputShape()))); + return references; + } + for (var member : shape.members()) { + references.addAll(getLocalReferences(context, member)); + } + + var shapeRefs = shape.getMemberTrait(model, ReferencesTrait.class); + var externalsRefs = context.settings().references(); + var serviceResources = TopDownIndex.of(model).getContainedResources(context.settings().service()) + .stream() + .map(Shape::getId) + .collect(Collectors.toSet()); + + if (shapeRefs.isPresent()) { + for (var reference : shapeRefs.get().getReferences()) { + if (serviceResources.contains(reference.getResource()) + || externalsRefs.containsKey(reference.getResource())) { + references.add(reference); + } else { + LOGGER.warning(String.format(""" + Unable to generate a reference link for `%s`, referenced by `%s`. Use the `references` \ + map in the generator settings to add a reference link.""", + reference.getResource(), shape.getId())); + } + } + } + return references; + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RequestCompressionInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RequestCompressionInterceptor.java new file mode 100644 index 00000000000..64fcb90504c --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RequestCompressionInterceptor.java @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import java.util.Optional; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.RequestCompressionTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information about request compression to operations with the + * + * requestCompression trait. + */ +@SmithyInternalApi +public final class RequestCompressionInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + public boolean isIntercepted(ShapeDetailsSection section) { + return section.shape().hasTrait(RequestCompressionTrait.class); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeDetailsSection section) { + var trait = section.shape().expectTrait(RequestCompressionTrait.class); + writer.openAdmonition(NoticeType.IMPORTANT); + + // Have particular support for single-element lists. + writer.putContext("encoding", trait.getEncodings().size() == 1 + ? Optional.of(trait.getEncodings().get(0)) + : Optional.empty()); + writer.putContext("encodings", trait.getEncodings()); + + writer.write(""" + This operation supports optional request compression using \ + ${?encoding}${encoding:L} encoding.${/encoding}\ + ${^encoding}one of the following priority-ordered encodings: \ + ${#encodings}${value:L}${^key.last}, ${/key.last}${/encodings}.${/encoding}"""); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RetryableInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RetryableInterceptor.java new file mode 100644 index 00000000000..a812fdc2f25 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/RetryableInterceptor.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.RetryableTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * This adds badges and notices to errors that are retryable. + */ +@SmithyInternalApi +public final class RetryableInterceptor implements CodeInterceptor.Prepender { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().hasTrait(RetryableTrait.class); + } + + @Override + public void prepend(DocWriter writer, ShapeSubheadingSection section) { + writer.writeBadge(NoticeType.IMPORTANT, "RETRYABLE").write("\n"); + if (section.shape().expectTrait(RetryableTrait.class).getThrottling()) { + writer.openAdmonition(NoticeType.NOTE); + writer.write(""" + This is a throttling error. Request retries in response to this error should use exponential + backoff with jitter. Clients should do this automatically. + """); + writer.closeAdmonition(); + } + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/SensitiveInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/SensitiveInterceptor.java new file mode 100644 index 00000000000..d4a0c7e70a1 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/SensitiveInterceptor.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.SensitiveTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds danger admonitions to shapes that have the + * + * sensitive trait. + */ +@SmithyInternalApi +public final class SensitiveInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), SensitiveTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + writer.openAdmonition(NoticeType.DANGER); + writer.write(""" + The data this contains is sensitive and MUST be handled with care. \ + It MUST NOT be exposed in things like exception messages or log \ + output, except for full wire logs."""); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/SinceInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/SinceInterceptor.java new file mode 100644 index 00000000000..809dcf0e1e5 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/SinceInterceptor.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.SinceTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds admonitions for when a shape was introduced based on the + * + * since trait. + */ +@SmithyInternalApi +public final class SinceInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), SinceTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + var trait = section.shape().getMemberTrait(section.context().model(), SinceTrait.class).get(); + writer.openAdmonition(NoticeType.NOTE); + writer.write("New in version $L.", trait.getValue()); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/SparseInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/SparseInterceptor.java new file mode 100644 index 00000000000..edcb8e784b1 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/SparseInterceptor.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.interceptors; + +import java.util.Locale; +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.SparseTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds an admonition to shapes that have the + * + * sparse trait. + */ +@SmithyInternalApi +public final class SparseInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + public boolean isIntercepted(ShapeDetailsSection section) { + return section.shape().getMemberTrait(section.context().model(), SparseTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeDetailsSection section) { + var target = section.shape().isMemberShape() + ? section.context().model().expectShape(section.shape().asMemberShape().get().getTarget()) + : section.shape(); + writer.writeWithNoFormatting(previousText); + writer.openAdmonition(NoticeType.NOTE); + writer.write("This $L may contain null values.", target.getType().toString().toLowerCase(Locale.ENGLISH)); + writer.closeAdmonition(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/StreamingInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/StreamingInterceptor.java new file mode 100644 index 00000000000..9c2ef44eab4 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/StreamingInterceptor.java @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeDetailsSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.RequiresLengthTrait; +import software.amazon.smithy.model.traits.StreamingTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds documentation for the streaming trait. + */ +@SmithyInternalApi +public class StreamingInterceptor implements CodeInterceptor.Appender { + @Override + public Class sectionType() { + return ShapeDetailsSection.class; + } + + @Override + public boolean isIntercepted(ShapeDetailsSection section) { + return section.shape().getMemberTrait(section.context().model(), StreamingTrait.class).isPresent(); + } + + @Override + public void append(DocWriter writer, ShapeDetailsSection section) { + var target = section.shape().asMemberShape() + .map(member -> section.context().model().expectShape(member.getTarget())) + .orElse(section.shape()); + if (target.isBlobShape()) { + writer.pushState(); + writer.putContext("requiresLength", target.hasTrait(RequiresLengthTrait.class)); + writer.openAdmonition(NoticeType.IMPORTANT); + writer.write(""" + The data in this member is potentially very large and therefore must be streamed and not \ + stored in memory.${?requiresLength} The size of the data must be known ahead of time.\ + ${/requiresLength} + """); + writer.closeAdmonition(); + writer.popState(); + return; + } + + // TODO: event streams + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/TimestampFormatInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/TimestampFormatInterceptor.java new file mode 100644 index 00000000000..58c6159e4d3 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/TimestampFormatInterceptor.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.TimestampFormatTrait; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a member's + * timestamp format to the {@link ProtocolSection} if the protocol supports it. + */ +@SmithyInternalApi +public final class TimestampFormatInterceptor extends ProtocolTraitInterceptor { + private static final Pair DATE_TIME_REF = Pair.of( + "RFC3339 date-time", "https://datatracker.ietf.org/doc/html/rfc3339.html#section-5.6" + ); + private static final Pair HTTP_DATE_REF = Pair.of( + "RFC7231 IMF-fixdate", "https://tools.ietf.org/html/rfc7231.html#section-7.1.1.1" + ); + + @Override + protected Class getTraitClass() { + return TimestampFormatTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return TimestampFormatTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, TimestampFormatTrait trait) { + writer.writeInline("$B ", "TimestampFormat:"); + switch (trait.getFormat()) { + case DATE_TIME -> writer.write("$R", DATE_TIME_REF); + case HTTP_DATE -> writer.write("$R", HTTP_DATE_REF); + case EPOCH_SECONDS -> writer.write("epoch seconds"); + default -> { + return; + } + } + writer.writeWithNoFormatting("\n" + previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/UniqueItemsInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/UniqueItemsInterceptor.java new file mode 100644 index 00000000000..9e7121cc388 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/UniqueItemsInterceptor.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.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.traits.UniqueItemsTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds an annotation to docs for lists with the + * + * uniqueItems trait. + */ +@SmithyInternalApi +public final class UniqueItemsInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), UniqueItemsTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + writer.write(""" + Items in this list MUST be unique. + + $L""", previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/UnstableInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/UnstableInterceptor.java new file mode 100644 index 00000000000..807772bd2b3 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/UnstableInterceptor.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.interceptors; + +import software.amazon.smithy.docgen.core.sections.ShapeSubheadingSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.traits.UnstableTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a warning admonition to shapes marked as + * unstable. + */ +@SmithyInternalApi +public final class UnstableInterceptor implements CodeInterceptor { + @Override + public Class sectionType() { + return ShapeSubheadingSection.class; + } + + @Override + public boolean isIntercepted(ShapeSubheadingSection section) { + return section.shape().getMemberTrait(section.context().model(), UnstableTrait.class).isPresent(); + } + + @Override + public void write(DocWriter writer, String previousText, ShapeSubheadingSection section) { + writer.openAdmonition(NoticeType.WARNING); + writer.write("This is unstable or experimental and MAY change in the future."); + writer.closeAdmonition(); + writer.writeWithNoFormatting(previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlAttributeInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlAttributeInterceptor.java new file mode 100644 index 00000000000..55b32adf8ae --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlAttributeInterceptor.java @@ -0,0 +1,41 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.XmlAttributeTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +// TODO: this needs to get added to operation input / output shapes too. +// It isn't right now because those shapes don't have their own doc pages. +/** + * Notes that a member is an + * + * xml attribute in the {@link ProtocolSection} if the protocol supports it. + */ +@SmithyInternalApi +public final class XmlAttributeInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return XmlAttributeTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return XmlAttributeTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, XmlAttributeTrait trait) { + writer.writeWithNoFormatting(previousText + "\n"); + writer.openAdmonition(NoticeType.IMPORTANT); + writer.write("This member represents an XML attribute rather than a nested tag."); + writer.closeAdmonition(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlFlattenedInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlFlattenedInterceptor.java new file mode 100644 index 00000000000..21edfed3d5d --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlFlattenedInterceptor.java @@ -0,0 +1,83 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.ProtocolDefinitionTrait; +import software.amazon.smithy.model.traits.XmlFlattenedTrait; +import software.amazon.smithy.utils.CodeInterceptor; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds information to the protocol section for members indicating whether they target + * a flat list/map or wrapped list/map depending on whether they have the + * + * xmlFlattened trait. + */ +@SmithyInternalApi +public class XmlFlattenedInterceptor implements CodeInterceptor { + private static final Pair WRAPPED_LIST_REF = Pair.of( + "wrapped", "https://smithy.io/2.0/spec/protocol-traits.html#wrapped-list-serialization" + ); + private static final Pair FLAT_LIST_REF = Pair.of( + "flat", "https://smithy.io/2.0/spec/protocol-traits.html#flattened-list-serialization" + ); + private static final Pair WRAPPED_MAP_REF = Pair.of( + "wrapped", "https://smithy.io/2.0/spec/protocol-traits.html#wrapped-map-serialization" + ); + private static final Pair FLAT_MAP_REF = Pair.of( + "flat", "https://smithy.io/2.0/spec/protocol-traits.html#flattened-map-serialization" + ); + + @Override + public Class sectionType() { + return ProtocolSection.class; + } + + @Override + public boolean isIntercepted(ProtocolSection section) { + if (!section.shape().isMemberShape()) { + return false; + } + + var protocolShape = section.context().model().expectShape(section.protocol()); + var protocolDefinition = protocolShape.expectTrait(ProtocolDefinitionTrait.class); + if (!protocolDefinition.getTraits().contains(XmlFlattenedTrait.ID)) { + return false; + } + + var target = section.context().model().expectShape(section.shape().asMemberShape().get().getTarget()); + return target.isListShape() || target.isMapShape(); + } + + @Override + public void write(DocWriter writer, String previousText, ProtocolSection section) { + writer.write(""" + Serialization type: $R + + $L""", getRef(section.context(), section.shape()), previousText); + + } + + private Pair getRef(DocGenerationContext context, Shape shape) { + var target = context.model().expectShape(shape.asMemberShape().get().getTarget()); + if (target.isMapShape()) { + if (shape.hasTrait(XmlFlattenedTrait.class)) { + return FLAT_MAP_REF; + } + return WRAPPED_MAP_REF; + } + + if (shape.hasTrait(XmlFlattenedTrait.class)) { + return FLAT_LIST_REF; + } + return WRAPPED_LIST_REF; + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlNameInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlNameInterceptor.java new file mode 100644 index 00000000000..6be7dbeeb01 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlNameInterceptor.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.XmlNameTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds a member's + * xmlName to the {@link ProtocolSection} if the protocol supports it. + */ +@SmithyInternalApi +public final class XmlNameInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return XmlNameTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return XmlNameTrait.ID; + } + + @Override + public boolean isIntercepted(ProtocolSection section) { + // The xmlName trait uniquely doesn't inherit values from the target as a member. + return super.isIntercepted(section) && section.shape().hasTrait(XmlNameTrait.class); + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, XmlNameTrait trait) { + writer.putContext("xmlTagName", "XML tag name:"); + writer.write(""" + ${xmlTagName:B} $` + + $L""", trait.getValue(), previousText); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlNamespaceInterceptor.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlNamespaceInterceptor.java new file mode 100644 index 00000000000..d29dd0f291c --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/interceptors/XmlNamespaceInterceptor.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.interceptors; + +import software.amazon.smithy.docgen.core.sections.ProtocolSection; +import software.amazon.smithy.docgen.core.writers.DocWriter; +import software.amazon.smithy.docgen.core.writers.DocWriter.NoticeType; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.traits.XmlNamespaceTrait; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Notes that a member needs an + * + * xml namespace in the {@link ProtocolSection} if the protocol supports it. + */ +@SmithyInternalApi +public final class XmlNamespaceInterceptor extends ProtocolTraitInterceptor { + @Override + protected Class getTraitClass() { + return XmlNamespaceTrait.class; + } + + @Override + protected ShapeId getTraitId() { + return XmlNamespaceTrait.ID; + } + + @Override + void write(DocWriter writer, String previousText, ProtocolSection section, XmlNamespaceTrait trait) { + writer.writeWithNoFormatting(previousText + "\n"); + var namespace = "xmlns"; + if (trait.getPrefix().isPresent()) { + namespace += ":" + trait.getPrefix().get(); + } + namespace += "=\"" + trait.getUri() + "\""; + writer.openAdmonition(NoticeType.IMPORTANT); + writer.write(""" + This tag must contain the following XML namespace $` + """, namespace); + writer.closeAdmonition(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/AuthSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/AuthSection.java new file mode 100644 index 00000000000..eaf44a4cbe0 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/AuthSection.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contains the documentation for the auth schemes that the service supports. + * + *

By default, the auth schemes are documented in a definition list. The title + * used for each auth scheme is the name that results from passing the auth trait's + * shape to the {@link software.amazon.smithy.docgen.core.DocSymbolProvider}. The + * name can be customized by decorating the provider with + * {@link software.amazon.smithy.docgen.core.DocIntegration#decorateSymbolProvider}. + * + *

The body of each auth scheme's docs is treated like a typical shape section, + * with a {@link ShapeSection}, {@link ShapeSubheadingSection}, + * {@link ShapeDetailsSection}, and documentation pulled from the shape. Details + * based on the trait's values can be inserted via one of those sections, intercepting + * when the shape's id matches the id of the auth trait's shape. + * + * @param context The context used to generate documentation. + * @param service The service whose documentation is being generated. + * + * @see ShapeSection to override documentation for individual auth schemes. + * @see software.amazon.smithy.docgen.core.interceptors.ApiKeyAuthInterceptor for an + * example of adding details to an auth trait's docs based on its values. + */ +@SmithyUnstableApi +public record AuthSection(DocGenerationContext context, ServiceShape service) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundOperationSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundOperationSection.java new file mode 100644 index 00000000000..be73f7587f4 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundOperationSection.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.EntityShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contains the documentation for individual operations bound to a service or resource. + * + *

Service documentation will include all operations transitively bound to the + * service, while resource documentation will only include operations directly + * bound to the resource through {@code operations} or {@code collectionOperations}. + * + * @param context The context used to generate documentation. + * @param container The service or resource the operation is bound to. + * @param operation The operation being listed. + * + * @see BoundOperationsSection For the section containing all operations bound to the + * service or resource. + * @see LifecycleOperationSection For operations bound to a resource's lifecycle + * operations. + * @see software.amazon.smithy.docgen.core.generators.ServiceGenerator for information + * about other sections present on the service's documentation page. + * @see software.amazon.smithy.docgen.core.generators.ResourceGenerator for information + * about other sections present on the documentation pages for resrouces. + */ +@SmithyUnstableApi +public record BoundOperationSection( + DocGenerationContext context, + EntityShape container, + OperationShape operation +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundOperationsSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundOperationsSection.java new file mode 100644 index 00000000000..f86907097b2 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundOperationsSection.java @@ -0,0 +1,40 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import java.util.List; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.EntityShape; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contains the documentation for all operations bound to a service or resource. + * + *

Service documentation will include all operations transitively bound to the + * service, while resource documentation will only include operations directly + * bound to the resource through {@code operations} or {@code collectionOperations}. + * + * @param context The context used to generate documentation. + * @param container The service or resource the operations are bound to. + * @param operations The operations being listed. + * + * @see BoundOperationSection For a section that only contains individual operations. + * @see LifecycleOperationSection For operations bound to a resource's lifecycle + * operations. + * @see software.amazon.smithy.docgen.core.generators.ServiceGenerator for information + * about other sections present on the service's documentation page. + * @see software.amazon.smithy.docgen.core.generators.ResourceGenerator for information + * about other sections present on the documentation pages for resrouces. + */ +@SmithyUnstableApi +public record BoundOperationsSection( + DocGenerationContext context, + EntityShape container, + List operations +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundResourceSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundResourceSection.java new file mode 100644 index 00000000000..341f4f62c98 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundResourceSection.java @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.EntityShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contains the documentation for individual (sub)resources bound to a service or + * resource. + * + *

Service documentation will include all resources transitively bound to the + * service, while resource documentation will only include sub-resources directly + * bound to the resource. + * + * @param context The context used to generate documentation. + * @param container The service or resource the (sub)resource is bound to. + * @param resource The (sub)resource being listed. + * + * @see BoundResourcesSection For the section containing all (sub)resources bound to + * the service or resource. + * @see software.amazon.smithy.docgen.core.generators.ServiceGenerator for information + * about other sections present on the service's documentation page. + * @see software.amazon.smithy.docgen.core.generators.ResourceGenerator for information + * about other sections present on the documentation pages for resrouces. + */ +@SmithyUnstableApi +public record BoundResourceSection( + DocGenerationContext context, + EntityShape container, + ResourceShape resource +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundResourcesSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundResourcesSection.java new file mode 100644 index 00000000000..c3eada6a09a --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/BoundResourcesSection.java @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import java.util.List; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.EntityShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contains the documentation for all (sub)resources bound to a service or resource. + * + *

Service documentation will include all resources transitively bound to the + * service, while resource documentation will only include sub-resources directly + * bound to the resource. + * + * @param context The context used to generate documentation. + * @param container The service or resource the (sub)resources are bound to. + * @param resources The (sub)resources being listed. + * + * @see BoundResourceSection For the section containing individual (sub)resources bound + * to the service or resource. + * @see software.amazon.smithy.docgen.core.generators.ServiceGenerator for information + * about other sections present on the service's documentation page. + * @see software.amazon.smithy.docgen.core.generators.ResourceGenerator for information + * about other sections present on the documentation pages for resrouces. + */ +@SmithyUnstableApi +public record BoundResourcesSection( + DocGenerationContext context, + EntityShape container, + List resources +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ErrorsSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ErrorsSection.java new file mode 100644 index 00000000000..5dad5ef6877 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ErrorsSection.java @@ -0,0 +1,28 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contains a listing of all the errors that an operation might throw, or errors common + * to a resource or service. + * + *

To simply add errors to a shape, instead use + * {@link software.amazon.smithy.docgen.core.DocIntegration#preprocessModel} to add + * them to the shape directly. + * + * @param context The context used to generate documentation. + * @param shape The shape whose errors are being documented. + * + * @see software.amazon.smithy.docgen.core.generators.OperationGenerator + */ +@SmithyUnstableApi +public record ErrorsSection(DocGenerationContext context, Shape shape) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ExampleSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ExampleSection.java new file mode 100644 index 00000000000..807442a34bc --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ExampleSection.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.traits.ExamplesTrait.Example; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates a single operation example as defined by the {@code examples} trait. + * + *

This modifies the contents of a single example. To modify the entire example + * section, use {@link ExamplesSection} instead. + * + * @param context The context used to generate documentation. + * @param operation The operation whose examples are being documented. + * @param example The example that will be documented. + * + * @see ExamplesSection + * @see software.amazon.smithy.docgen.core.generators.OperationGenerator + */ +@SmithyUnstableApi +public record ExampleSection( + DocGenerationContext context, + OperationShape operation, + Example example +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ExamplesSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ExamplesSection.java new file mode 100644 index 00000000000..36e93bda213 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ExamplesSection.java @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import java.util.List; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.traits.ExamplesTrait.Example; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates the documentation for an operation's examples as defined by the + * {@code example} trait. + * + *

This controls all the examples for an operation. To modify a single example, use + * {@link ExampleSection} instead. + * + * @param context The context used to generate documentation. + * @param operation The operation whose examples are being documented. + * @param examples The list of examples that will be documented. + * + * @see ExampleSection + * @see software.amazon.smithy.docgen.core.generators.OperationGenerator + */ +@SmithyUnstableApi +public record ExamplesSection( + DocGenerationContext context, + OperationShape operation, + List examples +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/LifecycleOperationSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/LifecycleOperationSection.java new file mode 100644 index 00000000000..d7232823092 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/LifecycleOperationSection.java @@ -0,0 +1,92 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contains the documentation for individual resource lifecycle operations. + * + * @param context The context used to generate documentation. + * @param resource The resource the operation is bound to. + * @param operation The lifecycle operation being listed. + * @param lifecycleType The type of lifecycle binding. + * + * @see LifecycleSection For the section containing all the resource lifecycle + * operations. + * @see BoundOperationSection For individual operations bound to the resource's + * {@code operations} or {@code collectionOperations} properties. + * @see BoundOperationsSection For all operations bound to the resource's + * {@code operations} or {@code collectionOperations} properties. + * @see software.amazon.smithy.docgen.core.generators.ResourceGenerator for information + * about other sections present on the documentation pages for resrouces. + */ +@SmithyUnstableApi +public record LifecycleOperationSection( + DocGenerationContext context, + ResourceShape resource, + OperationShape operation, + LifecycleType lifecycleType +) implements CodeSection { + + // smithy-model doesn't have a pared-down enum for these lifecycle types, instead + // using the broader RelationshipType. That's fine for smithy-model, which needs + // those other relationship types, but we don't want to confuse people here with + // tons of irrelevant enum values. So instead this pared-down enum is provided. + /** + * The type of lifecycle binding an operation can use. + * + * @see + * Smithy's resource lifecycle docs + */ + public enum LifecycleType { + /** + * Indicates the operation is bound as the resource's + * + * put operation. + */ + PUT, + + /** + * Indicates the operation is bound as the resource's + * + * create operation. + */ + CREATE, + + /** + * Indicates the operation is bound as the resource's + * + * read operation. + */ + READ, + + /** + * Indicates the operation is bound as the resource's + * + * update operation. + */ + UPDATE, + + /** + * Indicates the operation is bound as the resource's + * + * delete operation. + */ + DELETE, + + /** + * Indicates the operation is bound as the resource's + * + * list operation. + */ + LIST + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/LifecycleSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/LifecycleSection.java new file mode 100644 index 00000000000..96e04231020 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/LifecycleSection.java @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.ResourceShape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contains the documentation for all resource lifecycle operations. + * + * @param context The context used to generate documentation. + * @param resource The resource whose lifecycle operations are being documented. + * + * @see LifecycleSection For the section containing all the resource lifecycle + * operations. + * @see BoundOperationSection For individual operations bound to the resource's + * {@code operations} or {@code collectionOperations} properties. + * @see BoundOperationsSection For all operations bound to the resource's + * {@code operations} or {@code collectionOperations} properties. + * @see software.amazon.smithy.docgen.core.generators.ResourceGenerator for information + * about other sections present on the documentation pages for resrouces. + */ +@SmithyUnstableApi +public record LifecycleSection(DocGenerationContext context, ResourceShape resource) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/MemberSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/MemberSection.java new file mode 100644 index 00000000000..c6791e5ccf2 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/MemberSection.java @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Enables modifying or overwriting the documentation for a member. + * + * @param context The context used to generate documentation. + * @param member The member whose documentation is being generated. + * + * @see ShapeMembersSection to modify the listing of all members of the shape. + * @see ShapeSubheadingSection to add context immediately before the member's docs. + */ +@SmithyUnstableApi +public record MemberSection(DocGenerationContext context, MemberShape member) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ProtocolSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ProtocolSection.java new file mode 100644 index 00000000000..20fe054d444 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ProtocolSection.java @@ -0,0 +1,31 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A section that contains protocol-specific information for a specific protocol + * for a given shape. + * + * @param context The context used to generate documentation. + * @param shape The shape to add protocol information to. + * @param protocol The shape id of the protocol being documented. + * + * @see ProtocolsSection to make changes to all protocols and how they're displayed. + * @see ShapeSection to make non-protocol-specific changes to a shape's docs. + */ +@SmithyUnstableApi +public record ProtocolSection( + DocGenerationContext context, + Shape shape, + ShapeId protocol +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ProtocolsSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ProtocolsSection.java new file mode 100644 index 00000000000..b79858aeb70 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ProtocolsSection.java @@ -0,0 +1,31 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A section that contains all protocol-specific information for the given shape for + * all protocols. + * + *

Each individual protocol has its own {@link ProtocolSection}. If the service has + * more than one protocol, these sections will be in tabs. + * + * @param context The context used to generate documentation. + * @param shape The shape to add protocol information to. + * + * @see ProtocolSection to make additions to a particular protocol's section. + * @see ShapeSection to make non-protocol-specific changes to a shape's docs. + */ +@SmithyUnstableApi +public record ProtocolsSection( + DocGenerationContext context, + Shape shape +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeDetailsSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeDetailsSection.java new file mode 100644 index 00000000000..f953b8a3ce2 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeDetailsSection.java @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Enables injecting details immediately after a shape's modeled documentation. + * + * @param context The context used to generate documentation. + * @param shape The shape whose documentation is being generated. + * + * @see ShapeSection to modify the shape's entire documentation. + * @see ShapeSubheadingSection to inject docs before modeled documentation. + */ +@SmithyUnstableApi +public record ShapeDetailsSection(DocGenerationContext context, Shape shape) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeMembersSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeMembersSection.java new file mode 100644 index 00000000000..3197c9255fb --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeMembersSection.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import java.util.Collection; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.docgen.core.generators.MemberGenerator.MemberListingType; +import software.amazon.smithy.model.shapes.MemberShape; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates a listing of a shape's members. + * + * @param context The context used to generate documentation. + * @param shape The shape whose member documentation is being generated. + * @param members The members being generated. + * @param listingType The type of the listing. + * + * @see MemberSection to modify the documentation for an individual member. + */ +@SmithyUnstableApi +public record ShapeMembersSection( + DocGenerationContext context, + Shape shape, + Collection members, + MemberListingType listingType +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeSection.java new file mode 100644 index 00000000000..fd32cf3e4a3 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeSection.java @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates documentation for shapes. + * + * @param context The context used to generate documentation. + * @param shape The shape whose documentation is being generated. + * + * @see ShapeDetailsSection to insert details after the shape's modeled docs. + * @see ShapeMembersSection to modify the listing of the shape's members (if any). + * @see MemberSection to modify the documentation for an individual shape member. + */ +@SmithyUnstableApi +public record ShapeSection(DocGenerationContext context, Shape shape) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeSubheadingSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeSubheadingSection.java new file mode 100644 index 00000000000..d3fb47ac6ad --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeSubheadingSection.java @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Enables injecting details immediately before a shape's modeled documentation. + * + * @param context The context used to generate documentation. + * @param shape The shape whose documentation is being generated. + * + * @see ShapeSection to modify the shape's entire documentation. + * @see ShapeDetailsSection to inject docs after modeled documentation. + */ +@SmithyUnstableApi +public record ShapeSubheadingSection(DocGenerationContext context, Shape shape) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/ConfSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/ConfSection.java new file mode 100644 index 00000000000..932f359adb2 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/ConfSection.java @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections.sphinx; + +import java.util.Set; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates the {@code conf.py} file for sphinx. + * @see + * sphinx config docs + * @param context The context used to generate documentation. + * @param extensions Extensions needed to generate documentation. + */ +@SmithyUnstableApi +public record ConfSection(DocGenerationContext context, Set extensions) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/IndexSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/IndexSection.java new file mode 100644 index 00000000000..b7d4449f901 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/IndexSection.java @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections.sphinx; + +import java.nio.file.Path; +import java.util.Set; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Contains index files for sphinx. + * + *

These indexes are necessary to build up the left-side navigation bar. + * + * @param context The context used to generate documentation. + * @param directory The directory the index covers. + * @param sourceFiles The sphinx source files contained in the directory that need to + * be present in generated toctrees. + */ +@SmithyUnstableApi +public record IndexSection( + DocGenerationContext context, + Path directory, + Set sourceFiles +) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/MakefileSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/MakefileSection.java new file mode 100644 index 00000000000..f9098bee3a5 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/MakefileSection.java @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections.sphinx; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates a Makefile that wraps sphinx-build with default arguments. + * @param context The context used to generate documentation. + */ +@SmithyUnstableApi +public record MakefileSection(DocGenerationContext context) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/RequirementsSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/RequirementsSection.java new file mode 100644 index 00000000000..3d5f343246f --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/RequirementsSection.java @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections.sphinx; + +import java.util.Set; +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates a requirements file needed to install and run sphinx. + * + *

Any requirements added here will be installed in the environment used to + * automatically build the docs with {@code sphinx-build}. + * + * @param context The context used to generate documentation. + * @param requirements The requirements as a list of PEP 508 strings. + */ +@SmithyUnstableApi +public record RequirementsSection(DocGenerationContext context, Set requirements) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/WindowsMakeSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/WindowsMakeSection.java new file mode 100644 index 00000000000..3627d95eeb4 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/sphinx/WindowsMakeSection.java @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.sections.sphinx; + +import software.amazon.smithy.docgen.core.DocGenerationContext; +import software.amazon.smithy.utils.CodeSection; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Generates a batch script that wraps sphinx-build with default arguments. + * @param context The context used to generate documentation. + */ +@SmithyUnstableApi +public record WindowsMakeSection(DocGenerationContext context) implements CodeSection { +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/validation/DocValidationEventDecorator.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/validation/DocValidationEventDecorator.java new file mode 100644 index 00000000000..a3a4ddb278d --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/validation/DocValidationEventDecorator.java @@ -0,0 +1,41 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.validation; + +import software.amazon.smithy.model.traits.UnitTypeTrait; +import software.amazon.smithy.model.validation.Severity; +import software.amazon.smithy.model.validation.ValidationEvent; +import software.amazon.smithy.model.validation.ValidationEventDecorator; +import software.amazon.smithy.utils.SmithyInternalApi; + +/** + * Adds context to validation events describing how they impact docs. + */ +@SmithyInternalApi +public class DocValidationEventDecorator implements ValidationEventDecorator { + private static final String REUSE_DOCUMENTATION_CONTEXT = """ + Additionally, reusing your input and output structures can make your \ + documentation confusing for customers, because they'll see those \ + structures both as the inputs or outputs of your operation and as \ + standalone structures. This can be particularly confusing if not all of \ + your operation inputs and outputs do this."""; + + @Override + public boolean canDecorate(ValidationEvent event) { + return event.containsId("InputOutputStructureReuse"); + } + + @Override + public ValidationEvent decorate(ValidationEvent event) { + if (event.getShapeId().isPresent() && event.getShapeId().get().equals(UnitTypeTrait.UNIT)) { + return event.toBuilder().severity(Severity.SUPPRESSED).build(); + } + return event.toBuilder() + .message(event.getMessage() + " " + REUSE_DOCUMENTATION_CONTEXT) + .severity(Severity.DANGER) + .build(); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/DocImportContainer.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/DocImportContainer.java new file mode 100644 index 00000000000..7000922ae45 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/DocImportContainer.java @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.writers; + +import software.amazon.smithy.codegen.core.ImportContainer; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A No-Op import container. + */ +@SmithyUnstableApi +public class DocImportContainer implements ImportContainer { + @Override + public void importSymbol(Symbol symbol, String s) { + // no-op + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/DocWriter.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/DocWriter.java new file mode 100644 index 00000000000..a04fa81f6db --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/DocWriter.java @@ -0,0 +1,483 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.writers; + +import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.SymbolWriter; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.traits.DocumentationTrait; +import software.amazon.smithy.model.traits.StringTrait; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * A {@code SymbolWriter} provides abstract methods that will be used during + * documentation generation. This allows for other formats to be swapped out + * without much difficulty. + */ +@SmithyUnstableApi +public abstract class DocWriter extends SymbolWriter { + private static final int MAX_HEADING_DEPTH = 6; + + /** + * The full path to the file being written to by the writer. + */ + protected final String filename; + + private int headingDepth = 0; + + /** + * Constructor. + * + * @param importContainer The container to store any imports in. + * @param filename The name of the file being written. + */ + public DocWriter(DocImportContainer importContainer, String filename) { + super(importContainer); + this.filename = filename; + putFormatter('R', (s, i) -> referenceFormatter(s)); + putFormatter('B', (s, i) -> boldFormatter(s)); + putFormatter('`', (s, i) -> inlineLiteralFormatter(s)); + trimTrailingSpaces(); + } + + /** + * Formats the given reference object as a link if possible. + * + *

This given value can be expected to be one of the following types: + * + *

    + *
  • {@code Symbol}: The symbol's name is the link text and a combination of + * the definition file and {@link software.amazon.smithy.docgen.core.DocSymbolProvider#LINK_ID_PROPERTY} + * forms the actual link. If either the link id or definition file are not set, + * the formatter must return the symbol's name. + *
  • {@code SymbolReference}: The reference's alias is the link text and a + * combination of the referenced symbol's definition file and + * {@link software.amazon.smithy.docgen.core.DocSymbolProvider#LINK_ID_PROPERTY} + * forms the actual link. If either the link id or definition file are not set, + * the formatter should return the reference's alias. + *
  • {@code Pair}: The key is the link text and the value is + * the link. Both key and value MUST be present. + *
+ * + * @param value The value to format. + * @return returns a string formatted to reference the given value. + */ + abstract String referenceFormatter(Object value); + + /** + * Formats the given object as a bold string. + * + *

For example, a raw HTML writer might surround the given text with {@code b} tags. + * + * @param value The value to format. + * @return returns the value formatted as a bold string. + */ + abstract String boldFormatter(Object value); + + /** + * Formats the given object an inline literal. + * + *

This is the equivalent of surrounding text with backticks (`) in markdown. + * + * @param value The value to format. + * @return returns the value formatted an inline literal. + */ + abstract String inlineLiteralFormatter(Object value); + + /** + * Writes out the content of the shape's + * + * documentation trait, if present. + * + *

Smithy's documentation trait is in the + * CommonMark format, so writers + * for formats that aren't based on CommonMark will need to convert the value to + * their format. This includes raw HTML, which CommonMark allows. + * + *

If the shape doesn't have a documentation trait, the writer MAY write out + * default documentation. + * + * @param shape The shape whose documentation should be written. + * @param model The model whose documentation is being written. + * @return returns the writer. + */ + public DocWriter writeShapeDocs(Shape shape, Model model) { + var documentation = shape.getMemberTrait(model, DocumentationTrait.class).map(StringTrait::getValue) + .orElse("Placeholder documentation for `" + shape.getId() + "`"); + writeCommonMark(documentation.replace("$", "$$")); + return this; + } + + /** + * Writes documentation based on a commonmark string. + * + *

Smithy's documentation trait is in the + * CommonMark format, so writers + * for formats that aren't based on CommonMark will need to convert the value to + * their format. This includes raw HTML, which CommonMark allows. + * + * @param commmonMark A string containing CommonMark-formatted documentation. + * @return returns the writer. + */ + public abstract DocWriter writeCommonMark(String commmonMark); + + /** + * Writes a heading with the given content. + * + *

{@link #closeHeading} will be called to enable cleaning up any resources or + * context this method creates. + * + * @param content A string to use as the heading content. + * @return returns the writer. + */ + public DocWriter openHeading(String content) { + headingDepth++; + if (headingDepth > MAX_HEADING_DEPTH) { + throw new CodegenException(String.format( + "Tried opening a heading nested more deeply than the max depth of %d.", + MAX_HEADING_DEPTH + )); + } + return openHeading(content, headingDepth); + } + + /** + * Writes a heading with the given content and linkId. + * + *

{@link #closeHeading} will be called to enable cleaning up any resources or + * context this method creates. + * + * @param content A string to use as the heading content. + * @param linkId The identifier used to link to the heading. + * @return returns the writer. + */ + public DocWriter openHeading(String content, String linkId) { + return writeAnchor(linkId).openHeading(content); + } + + /** + * Writes a heading of a given level with the given content. + * + *

{@link #closeHeading} will be called to enable cleaning up any resources or + * context this method creates. + * + * @param content A string to use as the heading content. + * @param level The level of the heading to open. This corresponds to HTML heading + * levels, and will only have values between 1 and 6. + * @return returns the writer. + */ + abstract DocWriter openHeading(String content, int level); + + /** + * Closes the current heading, cleaning any context created for the current level, + * then writes a blank line. + * + * @return returns the writer. + */ + public DocWriter closeHeading() { + headingDepth--; + if (headingDepth < 0) { + throw new CodegenException( + "Attempted to close a heading when at the base heading level." + ); + } + write(""); + return this; + } + + /** + * Writes any context needed to open a definition list. + * + *

A definition list is a list where each element has an emphasized title or + * term. A basic way to represent this might be an unordered list where the term + * is followed by a colon. + * + *

This will primarily be used to list members, with the element titles being + * the member names, member types, and a link to those member types where + * applicable. It will also be used for resource lifecycle operations, which will + * have similar titles. + * + * @return returns the writer. + */ + public abstract DocWriter openDefinitionList(); + + /** + * Writes any context needed to close a definition list. + * + *

A definition list is a list where each element has an emphasized title or + * term. A basic way to represent this might be an unordered list where the term + * is followed by a colon. + * + *

This will primarily be used to list members, with the element titles being + * the member names, member types, and a link to those member types where + * applicable. It will also be used for resource lifecycle operations, which will + * have similar titles. + * + * @return returns the writer. + */ + public abstract DocWriter closeDefinitionList(); + + /** + * Writes any context needed to open a definition list item. + * + *

A definition list is a list where each element has an emphasized title or + * term. A basic way to represent this might be an unordered list where the term + * is followed by a colon. + * + *

This will primarily be used to list members, with the element titles being + * the member names, member types, and a link to those member types where + * applicable. It will also be used for resource lifecycle operations, which will + * have similar titles. + * + * @param titleWriter writes the title or term for the definition list item. + * @return returns the writer. + */ + public abstract DocWriter openDefinitionListItem(Consumer titleWriter); + + /** + * Writes any context needed to close a definition list item. + * + *

A definition list is a list where each element has an emphasized title or + * term. A basic way to represent this might be an unordered list where the term + * is followed by a colon. + * + *

This will primarily be used to list members, with the element titles being + * the member names, member types, and a link to those member types where + * applicable. It will also be used for resource lifecycle operations, which will + * have similar titles. + * + * @return returns the writer. + */ + public abstract DocWriter closeDefinitionListItem(); + + /** + * Writes a linkable element to the documentation with the given identifier. + * + *

The resulting HTML should be able to link to this anchor with {@code #linkId}. + * + *

For example, a direct HTML writer might create a {@code span} tag with + * the given string as the tag's {@code id}, or modify the next emitted tag + * to have the given id. + * + * @param linkId The anchor's link identifier. + * @return returns the writer. + */ + public abstract DocWriter writeAnchor(String linkId); + + /** + * Writes any opening context needed to form a tab group. + * + * @return returns the writer. + */ + public abstract DocWriter openTabGroup(); + + /** + * Writes any context needed to close a tab group. + * + * @return returns the writer. + */ + public abstract DocWriter closeTabGroup(); + + /** + * Writes any context needed to open a tab. + * + * @param title The title text that is displayed on the tab itself. + * @return returns the writer. + */ + public abstract DocWriter openTab(String title); + + /** + * Writes any context needed to close a tab. + * + * @return returns the writer. + */ + public abstract DocWriter closeTab(); + + /** + * Writes any context needed to open a code block. + * + *

For example, a pure HTML writer might write an opening {@code pre} tag. + * + * @param language the language of the block's code. + * @return returns the writer. + */ + public abstract DocWriter openCodeBlock(String language); + + /** + * Writes any context needed to close a code block. + * + *

For example, a pure HTML writer might write a closing {@code pre} tag. + * + * @return returns the writer. + */ + public abstract DocWriter closeCodeBlock(); + + /** + * Writes any context needed to open a code block tab. + * + * @param title The title text that is displayed on the tab itself. + * @param language the language of the tab's code. + * @return returns the writer. + */ + public DocWriter openCodeTab(String title, String language) { + return openTab(title).openCodeBlock(language); + } + + /** + * Writes any context needed to close a code block tab. + * + * @return returns the writer. + */ + public DocWriter closeCodeTab() { + return closeCodeBlock().closeTab(); + } + + /** + * Writes any context needed to open a list of the given type. + * + *

For example, a raw HTML writer might write an opening {@code ul} tag for + * an unordered list or an {@code ol} tag for an ordered list. + * + * @param listType The type of list to open. + * @return returns the writer. + */ + public abstract DocWriter openList(ListType listType); + + /** + * Writes any context needed to close a list of the given type. + * + *

For example, a raw HTML writer might write a closing {@code ul} tag for + * an unordered list or an {@code ol} tag for an ordered list. + * + * @param listType The type of list to close. + * @return returns the writer. + */ + public abstract DocWriter closeList(ListType listType); + + /** + * Writes any context needed to open a list item of the given type. + * + *

For example, a raw HTML writer might write an opening {@code li} tag for + * a list of any type. + * + * @param listType The type of list the item is a part of. + * @return returns the writer. + */ + public abstract DocWriter openListItem(ListType listType); + + /** + * Writes any context needed to close a list item of the given type. + * + *

For example, a raw HTML writer might write a closing {@code li} tag for + * a list of any type. + * + * @param listType The type of list the item is a part of. + * @return returns the writer. + */ + public abstract DocWriter closeListItem(ListType listType); + + /** + * Represents different types of lists. + */ + public enum ListType { + /** + * A list whose elements are ordered with numbers. + */ + ORDERED, + + /** + * A list whose elements don't have associated numbers. + */ + UNORDERED + } + + /** + * Opens an admonition with a custom title. + * + *

An admonition is an emphasized callout that typically have color-coded + * severity. A warning admonition, for example, might have a yellow or red + * banner that emphasizes the importance of the body text. + * + * @param type The type of admonition to open. + * @param titleWriter A consumer that writes out the title. + * @return returns the writer. + */ + public abstract DocWriter openAdmonition(NoticeType type, Consumer titleWriter); + + /** + * Opens an admonition with a default title. + * + *

An admonition is an emphasized callout that typically have color-coded + * severity. A warning admonition, for example, might have a yellow or red + * banner that emphasizes the importance of the body text. + * + * @param type The type of admonition to open. + * @return returns the writer. + */ + public abstract DocWriter openAdmonition(NoticeType type); + + /** + * Closes the body of an admonition. + * + * @return returns the writer. + */ + public abstract DocWriter closeAdmonition(); + + /** + * Writes text as a badge. + * + *

Implementations SHOULD write inline. + * + *

A badge in this context means text enclosed in a color-coded rectangular + * shape. The color should be based on the given type. + * + * @param type The notice type of the badge that determines styling. + * @param text The text to put in the badge. + * @return returns the writer. + */ + public abstract DocWriter writeBadge(NoticeType type, String text); + + /** + * The type of admonition. + * + *

This affects the default title of the admonition, as well as styling. + */ + @SmithyUnstableApi + public enum NoticeType { + /** + * An notice that adds context without any strong severity. + */ + NOTE, + + /** + * An notice that adds context which is important, but not severely so. + */ + IMPORTANT, + + /** + * A notice that adds context with strong severity. + * + *

This might be used by deprecation notices or required badges, for + * example. + */ + WARNING, + + /** + * A notice that adds context with extreme severity. + * + *

This might be used to add information about security-related concerns, + * such as sensitive shapes and members. + */ + DANGER, + + /** + * A notice that refers to external context or highlights information. + */ + INFO + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/MarkdownWriter.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/MarkdownWriter.java new file mode 100644 index 00000000000..0c04ad11a73 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/MarkdownWriter.java @@ -0,0 +1,243 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.writers; + +import static software.amazon.smithy.docgen.core.DocgenUtils.getSymbolLink; + +import java.nio.file.Paths; +import java.util.Optional; +import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.CodegenException; +import software.amazon.smithy.codegen.core.Symbol; +import software.amazon.smithy.codegen.core.SymbolReference; +import software.amazon.smithy.codegen.core.SymbolWriter; +import software.amazon.smithy.utils.Pair; +import software.amazon.smithy.utils.SmithyUnstableApi; +import software.amazon.smithy.utils.StringUtils; + +/** + * Writes documentation in CommonMark format. + */ +@SmithyUnstableApi +public class MarkdownWriter extends DocWriter { + + /** + * Constructs a MarkdownWriter. + * + * @param importContainer this file's import container. + * @param filename The full path to the file being written to. + */ + public MarkdownWriter(DocImportContainer importContainer, String filename) { + super(importContainer, filename); + } + + /** + * Constructs a MarkdownWriter. + * + * @param filename The full path to the file being written to. + */ + public MarkdownWriter(String filename) { + this(new DocImportContainer(), filename); + } + + /** + * Factory to construct {@code MarkdownWriter}s. + */ + public static final class Factory implements SymbolWriter.Factory { + @Override + public DocWriter apply(String filename, String namespace) { + return new MarkdownWriter(filename); + } + } + + @Override + String referenceFormatter(Object value) { + var reference = getReferencePair(value); + if (reference.getRight().isPresent()) { + return String.format("[%s](%s)", reference.getLeft(), reference.getRight().get()); + } else { + return reference.getLeft(); + } + } + + @Override + String boldFormatter(Object value) { + return String.format("**%s**", formatLiteral(value).replace("*", "\\*")); + } + + @Override + String inlineLiteralFormatter(Object value) { + return String.format("`%s`", formatLiteral(value).replace("`", "\\`")); + } + + private Pair> getReferencePair(Object value) { + String text; + Optional ref; + var relativeTo = Paths.get(filename); + if (value instanceof Optional optional && optional.isPresent()) { + return getReferencePair(optional.get()); + } else if (value instanceof Symbol symbolValue) { + text = symbolValue.getName(); + ref = getSymbolLink(symbolValue, relativeTo); + } else if (value instanceof SymbolReference referenceValue) { + text = referenceValue.getAlias(); + ref = getSymbolLink(referenceValue.getSymbol(), relativeTo); + } else if (value instanceof Pair pairValue) { + if (pairValue.getLeft() instanceof String left && pairValue.getRight() instanceof String right) { + text = left; + ref = Optional.of(right); + } else { + throw new CodegenException( + "Invalid type provided to $R. Expected both key and vale of the Pair to be Strings, but " + + "found " + value.getClass() + ); + } + } else { + throw new CodegenException( + "Invalid type provided to $R. Expected a Symbol, SymbolReference, or Pair, but " + + "found " + value.getClass() + ); + } + return Pair.of(text, ref); + } + + @Override + public DocWriter writeCommonMark(String commonMark) { + return writeWithNewline(commonMark); + } + + private DocWriter writeWithNewline(Object content, Object... args) { + write(content, args); + write(""); + return this; + } + + @Override + public DocWriter openHeading(String content, int level) { + writeWithNewline(StringUtils.repeat("#", level) + " " + content); + return this; + } + + @Override + public DocWriter openDefinitionList() { + return this; + } + + @Override + public DocWriter closeDefinitionList() { + return this; + } + + @Override + public DocWriter openDefinitionListItem(Consumer titleWriter) { + openListItem(ListType.UNORDERED); + writeInline("**$C** - ", titleWriter); + return this; + } + + @Override + public DocWriter closeDefinitionListItem() { + closeListItem(ListType.UNORDERED); + return this; + } + + @Override + public DocWriter writeAnchor(String linkId) { + // Anchors have no meaning in base markdown + return this; + } + + @Override + public DocWriter openTabGroup() { + return this; + } + + @Override + public DocWriter closeTabGroup() { + return this; + } + + @Override + public DocWriter openTab(String title) { + return write("- $L", title).indent(); + } + + @Override + public DocWriter closeTab() { + return dedent(); + } + + @Override + public DocWriter openCodeBlock(String language) { + return write("```$L", language); + } + + @Override + public DocWriter closeCodeBlock() { + return write("```"); + } + + @Override + public DocWriter openList(ListType listType) { + return this; + } + + @Override + public DocWriter closeList(ListType listType) { + return this; + } + + @Override + public DocWriter openListItem(ListType listType) { + if (listType == ListType.ORDERED) { + // We don't actually need to keep track of how far we are in the list because + // commonmark will render the list correctly so long as there is any number + // in front of the period. + writeInline("1. "); + } else { + writeInline("- "); + } + return indent(); + } + + @Override + public DocWriter closeListItem(ListType listType) { + return dedent(); + } + + @Override + public String toString() { + // Ensure there's exactly one trailing newline + return super.toString().stripTrailing() + "\n"; + } + + @Override + public DocWriter openAdmonition(NoticeType type, Consumer titleWriter) { + return writeInline("**$C:** ", titleWriter); + } + + @Override + public DocWriter openAdmonition(NoticeType type) { + return writeInline("**$L:** ", getAdmonitionName(type)); + } + + private String getAdmonitionName(NoticeType type) { + if (type.equals(NoticeType.INFO)) { + return "See Also"; + } + return type.toString(); + } + + @Override + public DocWriter closeAdmonition() { + return this; + } + + @Override + public DocWriter writeBadge(NoticeType type, String text) { + return writeInline("`$L`", text); + } +} diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/SphinxMarkdownWriter.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/SphinxMarkdownWriter.java new file mode 100644 index 00000000000..3d11512bb21 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/writers/SphinxMarkdownWriter.java @@ -0,0 +1,137 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core.writers; + +import java.util.Locale; +import java.util.function.Consumer; +import software.amazon.smithy.codegen.core.SymbolWriter; +import software.amazon.smithy.utils.SmithyUnstableApi; + +/** + * Writes documentation in CommonMark-based + * format for the Sphinx doc build system. + * + *

The specific markdown parser being written for is + * MyST with the + * following + * extensions enabled: {@code linkify} and {@code colon_fence} + */ +@SmithyUnstableApi +public final class SphinxMarkdownWriter extends MarkdownWriter { + + private boolean isNewTabGroup = true; + + /** + * Constructs a SphinxMarkdownWriter. + * + * @param filename The full path to the file being written to. + */ + public SphinxMarkdownWriter(String filename) { + super(filename); + } + + /** + * Factory to construct {@code SphinxMarkdownWriter}s. + */ + public static final class Factory implements SymbolWriter.Factory { + @Override + public DocWriter apply(String filename, String namespace) { + return new SphinxMarkdownWriter(filename); + } + } + + @Override + public DocWriter openDefinitionListItem(Consumer titleWriter) { + writeInline(""" + **$C** + :\s""", titleWriter); + indent(); + return this; + } + + @Override + public DocWriter closeDefinitionListItem() { + dedent(); + return this; + } + + @Override + public DocWriter writeAnchor(String linkId) { + write("($L)=", linkId); + return this; + } + + @Override + public DocWriter openTabGroup() { + isNewTabGroup = true; + return this; + } + + @Override + public DocWriter closeTabGroup() { + isNewTabGroup = true; + return this; + } + + @Override + public DocWriter openTab(String title) { + write(":::{tab} $L", title); + if (isNewTabGroup) { + // The inline tab plugin will automatically gather tabs into groups so long + // as no other elements separate them, so to make sure we never accidentally + // merge what should be two groups, we add this directive config to opening + // tabs to ensure a new group gets created. + write(":new-set:\n"); + isNewTabGroup = false; + } + return this; + } + + @Override + public DocWriter closeTab() { + return write(":::"); + } + + @Override + public DocWriter openAdmonition(NoticeType type, Consumer titleWriter) { + return write(""" + :::{admonition} $C + :class: $L + + """, titleWriter, getAdmonitionName(type)); + } + + @Override + public DocWriter openAdmonition(NoticeType type) { + return write(":::{$L}", getAdmonitionName(type)); + } + + private String getAdmonitionName(NoticeType type) { + if (type.equals(NoticeType.INFO)) { + return "seealso"; + } + return type.toString().toLowerCase(Locale.ENGLISH); + } + + @Override + public DocWriter closeAdmonition() { + write(":::"); + return this; + } + + @Override + public DocWriter writeBadge(NoticeType type, String text) { + switch (type) { + case NOTE -> writeInline("{bdg-primary}"); + case IMPORTANT -> writeInline("{bdg-success}"); + case WARNING -> writeInline("{bdg-warning}"); + case DANGER -> writeInline("{bdg-danger}"); + case INFO -> writeInline("{bdg-info}"); + default -> writeInline("{bdg}"); + } + return writeInline("`$L`", text); + } +} diff --git a/smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin b/smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin new file mode 100644 index 00000000000..72efe5bb5bb --- /dev/null +++ b/smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.build.SmithyBuildPlugin @@ -0,0 +1 @@ +software.amazon.smithy.docgen.core.SmithyDocPlugin diff --git a/smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.docgen.core.DocIntegration b/smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.docgen.core.DocIntegration new file mode 100644 index 00000000000..a9691f3a3ad --- /dev/null +++ b/smithy-docgen-core/src/main/resources/META-INF/services/software.amazon.smithy.docgen.core.DocIntegration @@ -0,0 +1,2 @@ +software.amazon.smithy.docgen.core.integrations.BuiltinsIntegration +software.amazon.smithy.docgen.core.integrations.SphinxIntegration diff --git a/smithy-docgen-core/src/main/resources/software/amazon/smithy/docgen/core/integrations/sphinx/requirements-base.txt b/smithy-docgen-core/src/main/resources/software/amazon/smithy/docgen/core/integrations/sphinx/requirements-base.txt new file mode 100644 index 00000000000..5a59592e16b --- /dev/null +++ b/smithy-docgen-core/src/main/resources/software/amazon/smithy/docgen/core/integrations/sphinx/requirements-base.txt @@ -0,0 +1,6 @@ +# These are base requirements needed for any sphinx project output. +Sphinx==8.1.3 +sphinx_inline_tabs==2023.4.21 +sphinx-copybutton==0.5.2 +Pygments==2.18.0 +sphinx-design==0.6.1 diff --git a/smithy-docgen-core/src/main/resources/software/amazon/smithy/docgen/core/integrations/sphinx/requirements-furo.txt b/smithy-docgen-core/src/main/resources/software/amazon/smithy/docgen/core/integrations/sphinx/requirements-furo.txt new file mode 100644 index 00000000000..aa3a5b7eda0 --- /dev/null +++ b/smithy-docgen-core/src/main/resources/software/amazon/smithy/docgen/core/integrations/sphinx/requirements-furo.txt @@ -0,0 +1,2 @@ +# These are requirements only needed for the base furo theme +furo==2024.8.6 diff --git a/smithy-docgen-core/src/main/resources/software/amazon/smithy/docgen/core/integrations/sphinx/requirements-markdown.txt b/smithy-docgen-core/src/main/resources/software/amazon/smithy/docgen/core/integrations/sphinx/requirements-markdown.txt new file mode 100644 index 00000000000..11903a85173 --- /dev/null +++ b/smithy-docgen-core/src/main/resources/software/amazon/smithy/docgen/core/integrations/sphinx/requirements-markdown.txt @@ -0,0 +1,3 @@ +# These are requirements needed for sphinx projects with markdown support. +myst-parser==4.0.0 +linkify-it-py==2.0.3 diff --git a/smithy-docgen-core/src/test/java/software/amazon/smithy/docgen/core/SmithyDocPluginTest.java b/smithy-docgen-core/src/test/java/software/amazon/smithy/docgen/core/SmithyDocPluginTest.java new file mode 100644 index 00000000000..bdf05e00d1d --- /dev/null +++ b/smithy-docgen-core/src/test/java/software/amazon/smithy/docgen/core/SmithyDocPluginTest.java @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.docgen.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import org.junit.jupiter.api.Test; +import software.amazon.smithy.build.PluginContext; +import software.amazon.smithy.build.SmithyBuildPlugin; +import software.amazon.smithy.build.MockManifest; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.utils.IoUtils; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Paths; + +public class SmithyDocPluginTest { + + @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); + + assertFalse(manifest.getFiles().isEmpty()); + assertServicePageContents(manifest); + } + + private void assertServicePageContents(MockManifest manifest) { + var actual = manifest.expectFileString("/content/index.md"); + var expected = readExpectedPageContent("expected-outputs/index.md"); + + assertEquals(expected, actual); + } + + private String readExpectedPageContent(String filename) { + URI uri; + + try { + uri = getClass().getResource(filename).toURI(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + + return IoUtils.readUtf8File(Paths.get(uri)) + .replace("\r\n", "\n"); + } +} diff --git a/smithy-docgen-core/src/test/resources/software/amazon/smithy/docgen/core/expected-outputs/index.md b/smithy-docgen-core/src/test/resources/software/amazon/smithy/docgen/core/expected-outputs/index.md new file mode 100644 index 00000000000..3a44728144d --- /dev/null +++ b/smithy-docgen-core/src/test/resources/software/amazon/smithy/docgen/core/expected-outputs/index.md @@ -0,0 +1,5 @@ +# Sample Service + +

This paragraph contains documentation about Sample Service. Sample Service is defined in the Smithy IDL. + For more information about Smithy IDL see + Smithy documentation.

diff --git a/smithy-docgen-core/src/test/resources/software/amazon/smithy/docgen/core/sample-service.smithy b/smithy-docgen-core/src/test/resources/software/amazon/smithy/docgen/core/sample-service.smithy new file mode 100644 index 00000000000..64ab1374115 --- /dev/null +++ b/smithy-docgen-core/src/test/resources/software/amazon/smithy/docgen/core/sample-service.smithy @@ -0,0 +1,11 @@ +$version: "2.0" + +namespace smithy.example + +///

This paragraph contains documentation about Sample Service. Sample Service is defined in the Smithy IDL. +/// For more information about Smithy IDL see +/// Smithy documentation.

+@title("Sample Service") +service SampleService { + +} diff --git a/smithy-docgen-test/build.gradle.kts b/smithy-docgen-test/build.gradle.kts new file mode 100644 index 00000000000..19274ab9e6e --- /dev/null +++ b/smithy-docgen-test/build.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +description = "This modules contains test cases for the Smithy DocGen project" + +extra["displayName"] = "Smithy :: DocGen :: Test" +extra["moduleName"] = "software.amazon.smithy.docgen.test" + +tasks.named("jar") { + enabled = false +} + +dependencies { + api(project(":smithy-docgen-core")) + api(project(":smithy-aws-traits")) + implementation(project(":smithy-docgen-core")) + implementation(project(":smithy-aws-traits")) +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} \ No newline at end of file diff --git a/smithy-docgen-test/model/main.smithy b/smithy-docgen-test/model/main.smithy new file mode 100644 index 00000000000..bb2c92a53c0 --- /dev/null +++ b/smithy-docgen-test/model/main.smithy @@ -0,0 +1,898 @@ +$version: "2.0" + +metadata suppressions = [ + { + id: "UnstableTrait" + namespace: "com.example" + reason: "These are used for examples." + } + { + id: "DeprecatedTrait" + namespace: "com.example" + reason: "These are used for examples." + } +] + +namespace com.example + +use aws.protocols#awsJson1_0 +use aws.protocols#restJson1 +use aws.protocols#restXml + +/// This is a sample service meant to exercise and demonstrate different kinds +/// of behavior that the documentation generator should handle. Click around to +/// different pages to see what it can do. Examples for various traits are +/// put into sections matching their sections in the Smithy docs. +/// +/// This service uses many different auth and protocol traits to demonstrate +/// how that will look, though in practice most services will only use one +/// or two of each. One important thing to note is the ordering of the +/// documented auth traits. By default it uses the same ordering as the +/// [auth trait](https://smithy.io/2.0/spec/authentication-traits.html#auth-trait) +/// if present. Anything not in that list is added to the end. On this service, +/// the only auth trait not present in the list is `httpBasicAuth`. +/// +/// For the most part, this doesn't go to any effort to pretend to be a real +/// service, operations and paramters will be named according to what they're +/// demonstrating rather than according to some kind of theme. +/// +/// One feature the implementation must support, for example, is the +/// ability to handle HTML tags in the documentation trait. This is because the +/// documentation trait uses the [CommonMark](https://spec.commonmark.org/) +/// format, which supports inline HTML. +@title("Documented Service") +@httpBasicAuth +@httpDigestAuth +@httpBearerAuth +@httpApiKeyAuth(name: "auth-bearing-header", in: "header", scheme: "Bearer") +@auth([httpApiKeyAuth, httpBearerAuth, httpDigestAuth]) +@awsJson1_0 +@restJson1 +@restXml +service DocumentedService { + version: "2023-10-13" + operations: [ + ConstraintTraits + TypeRefinementTraits + DocumentationTraits + BehaviorTraits + DefaultAuth + Unauthenticated + OptionalAuth + LimitedAuth + LimitedOptionalAuth + ProtocolTraits + StreamingTraits + HttpTraits + EndpointTraits + Misc + ExternalReference + ] + resources: [ + DocumentationResource + ] + errors: [ + ClientError + ServiceError + ] +} + +/// This operation showcases most of the various +/// [constraint traits](https://smithy.io/2.0/spec/constraint-traits.html). +/// +/// Note that the `idref` trait isn't supported since it's only used for traits +/// and doesn't reflect the API. The `private` trait similarly doesn't have +/// any impact on the API. +@http(method: "POST", uri: "/ConstraintTraits") +operation ConstraintTraits with [AllAuth] { + input := { + lengthExamples: LengthTraitExamples + + /// This member has a pattern trait on it. + @pattern("^[A-Za-z]+$") + pattern: String + + rangeExamples: RangeTraitExamples + + uniqueItems: StringSet + + enum: StringEnum + + intEnum: IntEnum + } +} + +/// This shows how the length trait is applied to various types. +structure LengthTraitExamples { + @length(min: 4) + string: String + + @length(max: 255) + blob: Blob + + @length(min: 2, max: 4) + list: StringSet + + map: StringMap +} + +/// A set of strings, using the +/// [uniqueItems](https://smithy.io/2.0/spec/constraint-traits.html#uniqueitems-trait) +/// trait to make the list effectively and ordered set. +@uniqueItems +list StringSet { + member: String +} + +/// A string map that allows null values. +@length(max: 16) +map StringMap { + key: String + value: String +} + +/// This shows how the range trait is applied to various types. +structure RangeTraitExamples { + @range(min: 0) + positive: Integer + + @range(max: 0) + negative: Long + + @range(min: 0, max: 255) + unsignedByte: Short +} + +/// This in an enum that can have one of the following values: +/// +/// - `FOO` +/// - `BAR` +/// +/// Like other shapes in the model, this doesn't actually mean anything. +enum StringEnum { + /// One of the more common placeholders in the programming world. + FOO + + /// Another very common placeholder, often seen with `foo`. + BAR +} + +/// This in an enum that can have one of the following values: +/// +/// - `SPAM`: `1` +/// - `EGGS`: `2` +/// +/// Like other shapes in the model, this doesn't actually mean anything. +intEnum IntEnum { + /// The spam and eggs placeholders are really only common in Python code bases. + SPAM = 1 + + /// They're a reference to a famous Monty Python skit, which is fitting because the + /// language itself is named after Monty Python. + EGGS = 2 +} + +/// This operation showcases the various +/// [type refinement traits](https://smithy.io/2.0/spec/type-refinement-traits.html). +/// +/// Note that `input` and `output` traits have the effect of moving the shape's +/// docs into the operation page with no dedicated shape page since it can't be +/// otherwise referenced. +/// +/// The `mixin` trait has no effect on the API, so it's not documented. The generator +/// will skip generating anything for them. +@http(method: "POST", uri: "/TypeRefinementTraits") +operation TypeRefinementTraits with [AllAuth] { + input := { + /// A boolean with a default value. + defaultBoolean: Boolean = true + + /// An enum with a default value. + defaultEnum: StringEnum = "FOO" + + /// An integer with a default that was addded after initial publication + /// and was annotated with the + /// [addedDefault trait](https://smithy.io/2.0/spec/type-refinement-traits.html#addeddefault-trait). + /// Currently that trait doesn't impact the docs. + @addedDefault + addedDefault: Integer = 5 + + /// A member that's required. Note that many of the resource operations + /// and http bindings will also have required traits since they need + /// them. + @required + required: String + + /// This member is optional for clients. Since this is API + /// documentation, that's not reflected here. + @clientOptional + @required + clientOptional: String + + enumValue: NonMatchingEnum + } + + output := { + sparseList: SparseList + sparseMap: SparseMap + } + + errors: [ + SimpleError + ] +} + +/// This string enum has values that don't match the member names. +enum NonMatchingEnum { + /// Note that the actual wire value is lower-case. + FOO = "foo" + + /// Note that the actual wire value is completely different. + BAR = "example" +} + +/// A list that allows null values. +@sparse +list SparseList { + member: String +} + +/// A map that allows null values. +@sparse +map SparseMap { + key: String + value: String +} + +/// This error is as basic as the protocols allow. +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([ + { + title: "Basic Example" + input: { + deprecated: { deprecatedSince: "foo" } + } + output: {} + } + { + title: "Error example" + documentation: "This shows an error response example." + input: { + deprecated: { deprecatedMessage: "bar" } + } + error: { + shapeId: SimpleError + content: { message: "That's super deprecated, don't use it." } + } + } +]) +@http(method: "POST", uri: "/DocumentationTraits") +operation DocumentationTraits with [AllAuth] { + input := { + noDocumentationTrait: Long + + deprecated: DeprecatedExamples + + /// While you can link things directly inside of the doc trait, you + /// can also used the external docs trait to provide see-also type + /// links. + @externalDocumentation("trait docs": "https://smithy.io/2.0/spec/documentation-traits.html#externaldocumentation-trait") + externalDocumentation: String + + /// This is distinct from required. + @recommended + recommended: String + + /// This has its own custom reason for why it's recommended. + @recommended(reason: "Because you can.") + reasonablyRecommended: String + + /// Sensitive data could be anything, but it shouldn't be logged. + sensitive: SensitiveBlob + + /// This indicates when it was added. + @since("2020-03-01") + since: Integer + + /// This has a number of tags, though said tags aren't part of the docs + /// and likely won't be. They're mostly useful for organizing the model + /// itself. + @tags(["foo", "bar"]) + tags: Short + } + + output := { + /// This integer being unstable could mean later it becomes an enum, or + /// a different number type, or evern be removed. + @unstable + unstable: Integer + } + + errors: [ + SimpleError + ] +} + +/// This showcases the different ways a shape can be marked as deprecated. +@deprecated +structure DeprecatedExamples { + @deprecated + deprecated: String + + @deprecated(since: "2020-03-01") + deprecatedSince: String + + @deprecated(message: "This gets a custom message.") + deprecatedMessage: String + + @deprecated(since: "2020-03-01", message: "This gets a custom message too.") + fullDeprecated: String +} + +@sensitive +blob SensitiveBlob + +/// This operation showcases the various +/// [behavior traits](https://smithy.io/2.0/spec/behavior-traits.html). +/// +/// See the various read operations in the resource examples for an +/// example of the `readonly` trait. Similarly, see the list operations for +/// examples of pagination. +@requestCompression( + encodings: ["gzip"] +) +@idempotent +@http(method: "POST", uri: "/BehaviorTraits") +operation BehaviorTraits with [AllAuth] { + input := { + /// This token is still required for services. + @idempotencyToken + idempotencyToken: String + } + + errors: [ + RetryableError + ] +} + +/// An error that's explicitly retryable. +@retryable(throttling: true) +@error("server") +@httpError(500) +structure RetryableError with [ErrorMixin] {} + +// This mixin provides all of the auth types available to the service, in the +// default expected priority. In effect, this hides the auth annotations that +// will otherwise appear since the service does not list all of its auth types +// in its auth trait list. +@mixin +@auth([httpApiKeyAuth, httpBearerAuth, httpDigestAuth, httpBasicAuth]) +operation AllAuth {} + +/// This operation uses the service's default auth list. Since that doesn't +/// include all of the possible auth types, this should display information +/// indicating what this operation supports. +/// +/// You may wonder why it's like this, and why the auth information doesn't +/// just show up when the operation's list differs from what is on the +/// service's auth list. The answer is that auth documenation is inherently +/// a top level affair, so all of the possible auth needs to be documented at +/// the top level. So when an operation doesn't support all the possible auth +/// types, that has to be called out. +@http(method: "POST", uri: "/DefaultAuth") +operation DefaultAuth {} + +/// This operation does not support any of the service's auth types. +@auth([]) +@http(method: "POST", uri: "/Unauthenticated") +operation Unauthenticated {} + +/// This operation supports all of the service's auth types, but optionally. +@optionalAuth +@http(method: "POST", uri: "/OptionalAuth") +operation OptionalAuth with [AllAuth] {} + +/// This operation supports a limited set of the service's auth. +@auth([httpBasicAuth, httpApiKeyAuth]) +@http(method: "POST", uri: "/LimitedAuth") +operation LimitedAuth {} + +/// This operation supports a limited set of the service's auth, optionally. +@optionalAuth +@auth([httpBasicAuth, httpDigestAuth]) +@http(method: "POST", uri: "/LimitedOptionalAuth") +operation LimitedOptionalAuth {} + +/// This operation showcases the various +/// [serialization and protocol traits](https://smithy.io/2.0/spec/protocol-traits.html). +@http(method: "POST", uri: "/ProtocolTraits") +operation ProtocolTraits with [AllAuth] { + input := { + /// This has a name representation in JSON that differs from the member name. + @jsonName("spam") + jsonName: String + + /// This targets a shape with a media type. + mediaType: VideoData + + /// This is a timestamp with a custom format. + timestamp: DateTime + + /// This is a timestamp without a custom format. + plainTimestamp: Timestamp + + xmlTraits: XmlTraits + + /// This member has different traits for the different protocols. + @jsonName("foo") + @xmlName("bar") + differentProtocols: String + } +} + +@mediaType("video/quicktime") +blob VideoData + +/// Timestamp in RFC3339 format +@timestampFormat("date-time") +timestamp DateTime + +/// This structure showcases various XML traits +@xmlName("foo") +structure XmlTraits { + /// This shows that the xml name isn't inherited from the target. + nested: XmlTraits + + /// This shows an xml name targeting a normal shape. + @xmlName("bar") + xmlName: String + + /// This shows how xml attributes are displayed. + @xmlAttribute + xmlAttribute: String + + /// This list uses the default nesting behavior. + nestedList: StringList + + /// This list uses the non-default flat list behavior. + @xmlFlattened + flatList: StringList + + /// This map uses the default nesting behavior. + nestedMap: StringMap + + /// This map uses the non-default flat map behavior. + @xmlFlattened + flatMap: StringMap + + /// This string tag needs an xml namespace added to it. + @xmlNamespace(prefix: "example", uri: "https://example.com") + xmlNamespace: String +} + +list StringList { + member: String +} + +/// This showcases the `streaming` trait with a data stream. +@http(method: "POST", uri: "/StreamingTraits") +operation StreamingTraits with [AllAuth] { + input := { + @httpPayload + output: StreamingBlob = "" + } +} + +/// This is a streaming blob. +@streaming +@requiresLength +blob StreamingBlob + +// TODO: add event stream traits +/// This operation showcases most of the various +/// [HTTP traits](https://smithy.io/2.0/spec/http-bindings.html). +@http(method: "POST", uri: "/HttpTraits/{label}/{greedyLabel+}?static", code: 200) +@httpChecksumRequired +operation HttpTraits with [AllAuth] { + input := { + /// This is a label member that's bound to a normal label. + @httpLabel + @required + label: String + + /// This is a label member that's bound to a greedy label. + @httpLabel + @required + greedyLabel: String + + /// This is a header member bound to a single static header. + @httpHeader("x-custom-header") + singletonHeader: String + + /// This is a header member that's bound to a list. + @httpHeader("x-list-header") + listHeader: StringList + + /// This is a header member that's bound to a map with a prefix. + @httpPrefixHeaders("prefix-") + prefixHeaders: StringMap + + /// This is a query param that's bound to a single param. + @httpQuery("singelton") + singletonQuery: String + + /// This is a query param that's bound to a list. + @httpQuery("list") + listQuery: StringList + + /// This is an open listing of all query params. + @httpQueryParams + mapQuery: StringMap + + /// This is the operation's payload, only useable since everything + /// else is bound to some other part of the HTTP request. + @httpPayload + payload: Blob + } + + output := { + /// This allows people to more easily interact with the http response + /// without having to leak the response object. + @httpResponseCode + responseCode: Integer + } +} + +/// The endpoint traits are currently not supported. +@endpoint(hostPrefix: "{foo}.data.") +@http(method: "POST", uri: "/EndpointTraits") +operation EndpointTraits with [AllAuth] { + input := { + /// This will be sent both as part of the endpoint prefix and in the + /// message body. + @required + @hostLabel + foo: String + } +} + +/// This operation showcases anything that doesn't fit cleanly into one of the +/// other showcase operations. +@http(method: "POST", uri: "/Misc") +operation Misc with [AllAuth] { + input := { + union: DocumentedUnion + } +} + +/// Unions can only have one member set. The member name is used as a tag to +/// determine which member is intended at runtime. +union DocumentedUnion { + /// Union members for the most part look like structure members, with the exception + /// that exactly one must be set. + string: String + + /// It doesn't matter that multiple members target the same type, since the type + /// isn't the discriminator, the tag (member name) is. + otherString: String +} + +@mixin +@error("client") +@httpError(400) +structure ErrorMixin { + /// The wire-level error identifier. + code: String + + /// A message with details about why the error happened. + message: String +} + +/// This is an error that is the fault of the calling client. +structure ClientError with [ErrorMixin] {} + +/// This is an error that is the fault of the service. +@error("server") +@httpError(500) +structure ServiceError with [ErrorMixin] {} + +/// This operation references a resource shape that isn't contained within this +/// model, and so generating a reference link to it requires configuring the +/// `references` setting of the generator. +@http(method: "POST", uri: "/ExternalReference") +operation ExternalReference { + input := { + /// A structure that contains the identifiers for the external resource. + externalReference: ExternalResourceReference + } +} + +/// This is a non-input, non-output structure that contains references, in this +/// case to an external resource. +@references([ + { + resource: "com.example#ExternalResource" + rel: "help" + } +]) +structure ExternalResourceReference { + /// This is the actual identifier for the external resource. + externalResourceId: String +} + +/// A resource shape. To have some sense of readability this will represent the concept +/// of documentation itself as a resource, presenting the image of a service which +/// stores such things. +@noReplace +resource DocumentationResource { + identifiers: { + id: DocumentationId + } + properties: { + contents: DocumentationContents + archived: DocumentationArchived + } + put: PutDocs + create: CreateDocs + read: GetDocs + update: UpdateDocs + delete: DeleteDocs + list: ListDocs + operations: [ + ArchiveDocs + ] + collectionOperations: [ + DeleteArchivedDocs + ] + resources: [ + DocumentationArtifact + ] +} + +/// The identifier for the documentation resoruce. +/// +/// These properites and identifiers all have their own shapes to enable documentation +/// sharing, not necessarily because they have meaningful collections of constraints +/// or other wire-level traits. +@references([ + { + resource: DocumentationResource + } +]) +string DocumentationId + +/// The actual body of the documentation. +string DocumentationContents + +/// Whether or not the documentation has been archived. This could mean that changes +/// are rejected, for example. +boolean DocumentationArchived + +/// Put operations create a resource with a user-specified identifier. +@idempotent +@http(method: "PUT", uri: "/DocumentationResource/{id}") +operation PutDocs with [AllAuth] { + input := for DocumentationResource { + @required + @httpLabel + $id + + @required + $contents + } +} + +/// Create operations instead have the service create the identifier. +@http(method: "POST", uri: "/DocumentationResource") +operation CreateDocs with [AllAuth] { + input := for DocumentationResource { + @required + $contents + } +} + +/// Gets the contents of a documentation resource. +@readonly +@http(method: "GET", uri: "/DocumentationResource/{id}") +operation GetDocs with [AllAuth] { + input := for DocumentationResource { + @required + @httpLabel + $id + } + + output := for DocumentationResource { + @required + $id + + @required + $contents + + @required + $archived + } +} + +/// Does an update on the documentation resource. These can often also be the put +/// lifecycle operation. +@idempotent +@http(method: "PUT", uri: "/DocumentationResource") +operation UpdateDocs with [AllAuth] { + input := for DocumentationResource { + @required + @httpQuery("id") + $id + + @required + $contents + } +} + +/// Deletes documentation. +@idempotent +@http(method: "DELETE", uri: "/DocumentationResource/{id}") +operation DeleteDocs with [AllAuth] { + input := for DocumentationResource { + @required + @httpLabel + $id + } +} + +/// Archives documentation. This is here to be a non-lifecycle instance operation. +/// We need both instance operations and collection operations that aren't lifecycle +/// operations to make sure both cases are being documented. +@idempotent +@http(method: "PUT", uri: "/DocumentationResource/{id}/archive") +operation ArchiveDocs with [AllAuth] { + input := for DocumentationResource { + @required + @httpLabel + $id + } +} + +/// Deletes all documentation that's been archived. This is a collection operation that +/// isn't part of a lifecycle operation, which is again needed to make sure everything +/// is being documented as expected. +@http(method: "DELETE", uri: "/DocumentationResource?delete-archived") +@idempotent +operation DeleteArchivedDocs with [AllAuth] {} + +/// Lists the avialable documentation resources. +@readonly +@http(method: "GET", uri: "/DocumentationResource") +@paginated(inputToken: "paginationToken", outputToken: "paginationToken", items: "documentation", pageSize: "pageSize") +operation ListDocs with [AllAuth] { + input := { + /// Whether to list documentation that has been archived. + @httpQuery("showArchived") + showArchived: Boolean = false + + @httpHeader("x-example-pagination-token") + paginationToken: String + + @httpHeader("x-example-page-size") + pageSize: Integer + } + + output := { + @required + documentation: DocumentationList + + paginationToken: String + } +} + +list DocumentationList { + member: Documentation +} + +/// A concrete documentation resource instance. +structure Documentation for DocumentationResource { + @required + $id + + @required + $contents + + @required + $archived +} + +/// This would be something like a built PDF. +resource DocumentationArtifact { + identifiers: { + id: DocumentationId + artifactId: DocumentationArtifactId + } + properties: { + data: DocumentationArtifactData + } + put: PutDocArtifact + read: GetDocArtifact + delete: DeleteDocArtifact +} + +/// Sub-resources need distinct identifiers. +string DocumentationArtifactId + +@mixin +@references([ + { + resource: DocumentationArtifact + } +]) +structure DocArtifactRef for DocumentationArtifact { + $id + $artifactId +} + +/// This would be the bytes containing the artifact +blob DocumentationArtifactData + +@idempotent +@http(method: "PUT", uri: "/DocumentationResource/{id}/artifact/{artifactId}") +operation PutDocArtifact with [AllAuth] { + input := for DocumentationArtifact with [DocArtifactRef] { + @required + @httpLabel + $id + + @required + @httpLabel + $artifactId + + @required + $data + } +} + +@readonly +@http(method: "GET", uri: "/DocumentationResource/{id}/artifact/{artifactId}") +operation GetDocArtifact with [AllAuth] { + input := for DocumentationArtifact with [DocArtifactRef] { + @required + @httpLabel + $id + + @required + @httpLabel + $artifactId + } + + output := for DocumentationArtifact with [DocArtifactRef] { + @required + $id + + @required + $artifactId + + @required + $data + } +} + +@idempotent +@http(method: "DELETE", uri: "/DocumentationResource/{id}/artifact/{artifactId}") +operation DeleteDocArtifact with [AllAuth] { + input := for DocumentationArtifact with [DocArtifactRef] { + @required + @httpLabel + $id + + @required + @httpLabel + $artifactId + } +} diff --git a/smithy-docgen-test/smithy-build.json b/smithy-docgen-test/smithy-build.json new file mode 100644 index 00000000000..fdf8939ccea --- /dev/null +++ b/smithy-docgen-test/smithy-build.json @@ -0,0 +1,27 @@ +{ + "version": "1.0", + "projections": { + "plain-markdown": { + "plugins": { + "docgen": { + "service": "com.example#DocumentedService", + "format": "markdown", + "references": { + "com.example#ExternalResource": "https://aws.amazon.com" + } + } + } + }, + "sphinx-markdown": { + "plugins": { + "docgen": { + "service": "com.example#DocumentedService", + "format": "sphinx-markdown", + "references": { + "com.example#ExternalResource": "https://aws.amazon.com" + } + } + } + } + } +}