diff --git a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DirectedDocGen.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DirectedDocGen.java index 8571f39..2b97d28 100644 --- a/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DirectedDocGen.java +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/DirectedDocGen.java @@ -12,10 +12,12 @@ 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.OperationGenerator; import software.amazon.smithy.docgen.core.generators.ServiceGenerator; import software.amazon.smithy.docgen.core.generators.StructureGenerator; import software.amazon.smithy.utils.SmithyUnstableApi; @@ -52,6 +54,11 @@ public void generateStructure(GenerateStructureDirective directive) { + new OperationGenerator().accept(directive); + } + @Override public void generateError(GenerateErrorDirective directive) { // no-op for now 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 index e8468d9..2add351 100644 --- 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 @@ -7,18 +7,25 @@ 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.MemberShape; +import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.model.shapes.Shape; +import software.amazon.smithy.model.shapes.ShapeId; import software.amazon.smithy.model.shapes.ShapeVisitor; import software.amazon.smithy.model.shapes.StructureShape; +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.utils.SmithyUnstableApi; @@ -35,20 +42,28 @@ * 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 @@ -74,6 +89,17 @@ public final class DocSymbolProvider extends ShapeVisitor.Default implem */ 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 @@ -104,6 +130,7 @@ public final class DocSymbolProvider extends ShapeVisitor.Default implem private final Model model; private final DocSettings docSettings; private final ServiceShape serviceShape; + private final Map ioToOperation; /** * Constructor. @@ -115,6 +142,26 @@ 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 @@ -132,12 +179,26 @@ public Symbol serviceShape(ServiceShape shape) { } @Override - public Symbol structureShape(StructureShape shape) { + public Symbol operationShape(OperationShape shape) { return getSymbolBuilder(shape) .definitionFile(getDefinitionFile(serviceShape, shape)) .build(); } + @Override + public Symbol structureShape(StructureShape shape) { + var builder = getSymbolBuilder(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); + } else { + builder.definitionFile(getDefinitionFile(serviceShape, shape)); + } + return builder.build(); + } + @Override public Symbol memberShape(MemberShape shape) { var builder = getSymbolBuilder(shape) 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 index 848c8de..a4da221 100644 --- 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 @@ -5,8 +5,11 @@ package software.amazon.smithy.docgen.core.generators; +import java.util.Collection; +import java.util.Locale; import software.amazon.smithy.codegen.core.CodegenException; 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.ShapeMembersSection; import software.amazon.smithy.docgen.core.writers.DocWriter; @@ -82,25 +85,44 @@ public MemberGenerator( @Override public void run() { - writer.pushState(new ShapeMembersSection(context, shape, shape.members(), listingType)); - writer.openHeading(listingType.getTitle()); - writer.openMemberListing(); - for (MemberShape member: shape.getAllMembers().values()) { - writer.pushState(new MemberSection(context, member)); - - var symbol = context.symbolProvider().toSymbol(member); - var target = context.model().expectShape(member.getTarget()); - writer.openMemberEntry(symbol, w -> target.accept(new MemberTypeVisitor(w, context))); - writer.writeShapeDocs(member, context.model()); - - writer.closeMemberEntry(); - writer.popState(); + 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.openMemberListing(); + for (MemberShape member : members) { + writer.pushState(new MemberSection(context, member)); + + var symbol = context.symbolProvider().toSymbol(member); + var target = context.model().expectShape(member.getTarget()); + writer.openMemberEntry(symbol, w -> target.accept(new MemberTypeVisitor(w, context))); + writer.writeShapeDocs(member, context.model()); + + writer.closeMemberEntry(); + writer.popState(); + } + writer.closeMemberListing(); + writer.closeHeading(); } - writer.closeMemberListing(); - 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(); + default -> shape.getAllMembers().values(); + }; + } + /** * The type of listing. This controls the heading title and anchor id for the section. */ @@ -108,12 +130,24 @@ public enum MemberListingType { /** * Indicates the listing is for normal shape members. */ - MEMBERS("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"); private final String title; + private final String linkIdSuffix; MemberListingType(String title) { this.title = title; + this.linkIdSuffix = title.toLowerCase(Locale.ENGLISH).strip().replaceAll("\\s+", "-"); } /** @@ -122,6 +156,14 @@ public enum MemberListingType { 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 { 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 0000000..4abeb86 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/generators/OperationGenerator.java @@ -0,0 +1,166 @@ +/* + * 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.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; + +/** + * 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 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}. + *
    + * + * 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. + */ +public 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.writeShapeDocs(operation, directive.model()); + writer.injectSection(new ShapeDetailsSection(context, 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) { + writer.openListItem(ListType.UNORDERED); + writer.write("$R", context.symbolProvider().toSymbol(context.model().expectShape(error))); + 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)); + 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/integrations/SphinxIntegration.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/integrations/SphinxIntegration.java index a75184e..5242dd5 100644 --- 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 @@ -88,7 +88,8 @@ public final class SphinxIntegration implements DocIntegration { private static final List BASE_REQUIREMENTS = List.of( "Sphinx==7.2.6", "myst-parser==2.0.0", - "linkify-it-py==2.0.2" + "linkify-it-py==2.0.2", + "sphinx-inline-tabs==2023.4.21" ); private SphinxSettings settings = SphinxSettings.fromNode(Node.objectNode()); @@ -163,7 +164,10 @@ private void writeConf(DocGenerationContext context) { if (context.docFormat().name().equals(MARKDOWN_FORMAT)) { writer.write(""" - extensions = ["myst_parser"] + extensions = [ + "myst_parser", + "sphinx_inline_tabs" + ] myst_enable_extensions = [ # Makes bare links into actual links "linkify", 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 0000000..4359c83 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ErrorsSection.java @@ -0,0 +1,26 @@ +/* + * 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; + +/** + * 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 + */ +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 0000000..0ddb936 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ExampleSection.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.OperationShape; +import software.amazon.smithy.model.traits.ExamplesTrait.Example; +import software.amazon.smithy.utils.CodeSection; + +/** + * 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 + */ +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 0000000..79ecb65 --- /dev/null +++ b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ExamplesSection.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.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; + +/** + * 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 + */ +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/ShapeSection.java b/smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/sections/ShapeSection.java index 4955d8a..fd32cf3 100644 --- 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 @@ -6,12 +6,12 @@ package software.amazon.smithy.docgen.core.sections; import software.amazon.smithy.docgen.core.DocGenerationContext; -import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.utils.CodeSection; import software.amazon.smithy.utils.SmithyUnstableApi; /** - * Generates documentation for non-service shapes. + * Generates documentation for shapes. * * @param context The context used to generate documentation. * @param shape The shape whose documentation is being generated. @@ -21,5 +21,5 @@ * @see MemberSection to modify the documentation for an individual shape member. */ @SmithyUnstableApi -public record ShapeSection(DocGenerationContext context, StructureShape shape) implements CodeSection { +public record ShapeSection(DocGenerationContext context, Shape shape) implements CodeSection { } 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 index 4820d08..0517e8d 100644 --- 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 @@ -11,6 +11,8 @@ 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; /** @@ -82,7 +84,25 @@ public DocWriter(DocImportContainer importContainer, String filename) { * @param model The model whose documentation is being written. * @return returns the writer. */ - public abstract DocWriter writeShapeDocs(Shape shape, Model model); + 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. @@ -207,4 +227,131 @@ public DocWriter closeHeading() { * @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 + } } 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 index 9a09ed3..3a92beb 100644 --- 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 @@ -14,10 +14,6 @@ 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.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.Pair; import software.amazon.smithy.utils.SmithyUnstableApi; import software.amazon.smithy.utils.StringUtils; @@ -97,17 +93,8 @@ private Pair> getReferencePair(Object value) { } @Override - public DocWriter writeShapeDocs(Shape shape, Model model) { - Optional docTrait; - if (shape.isMemberShape()) { - docTrait = shape.asMemberShape().get().getMemberTrait(model, DocumentationTrait.class); - } else { - docTrait = shape.getTrait(DocumentationTrait.class); - } - var documentation = docTrait.map(StringTrait::getValue) - .orElse("Placeholder documentation for `" + shape.getId() + "`"); - writeWithNewline(documentation.replace("$", "$$")); - return this; + public DocWriter writeCommonMark(String commonMark) { + return writeWithNewline(commonMark); } private DocWriter writeWithNewline(Object content, Object... args) { @@ -134,13 +121,14 @@ public DocWriter closeMemberListing() { @Override public DocWriter openMemberEntry(Symbol memberSymbol, Consumer writeType) { - writeInline("- **$L** (*$C*): ", memberSymbol.getName(), writeType); + openListItem(ListType.UNORDERED); + writeInline("**$L** (*$C*): ", memberSymbol.getName(), writeType); return this; } @Override public DocWriter closeMemberEntry() { - return this; + return closeListItem(ListType.UNORDERED); } @Override @@ -149,6 +137,64 @@ public DocWriter writeAnchor(String linkId) { 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 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 index d06a87a..acf1f83 100644 --- 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 @@ -23,6 +23,8 @@ @SmithyUnstableApi public final class SphinxMarkdownWriter extends MarkdownWriter { + private boolean isNewTabGroup = true; + /** * Constructs a SphinxMarkdownWriter. * @@ -60,4 +62,35 @@ 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(":::"); + } } diff --git a/smithy-docgen-test/model/main.smithy b/smithy-docgen-test/model/main.smithy index 8f71cf2..b878553 100644 --- a/smithy-docgen-test/model/main.smithy +++ b/smithy-docgen-test/model/main.smithy @@ -12,8 +12,36 @@ service DocumentedService { operations: [ DocumentedOperation ] + errors: [ + ClientError + ServiceError + ] } +@examples( + [ + { + title: "Basic Example" + documentation: "This **MUST** also support CommonMark" + input: { + structure: { + string: "foo" + integer: 4 + enum: "BAR" + undocumented: {boolean: false} + } + } + output: { + structure: { + string: "spam" + integer: 8 + enum: "FOO" + undocumented: {boolean: true} + } + } + } + ] +) operation DocumentedOperation { input := { structure: DocumentedStructure @@ -21,6 +49,9 @@ operation DocumentedOperation { output := { structure: DocumentedStructure } + errors: [ + DocumentedOperationError + ] } /// This structure is an example documentable structure with several members that @@ -64,3 +95,23 @@ structure UndocumentedStructure { blob: Blob boolean: Boolean } + +@mixin +@error("client") +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") +structure ServiceError with [ErrorMixin] {} + +/// This error is only returned by DocumentedOperation +structure DocumentedOperationError with [ErrorMixin] {}