Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.python.aws.codegen;

import static software.amazon.smithy.python.codegen.SymbolProperties.OPERATION_METHOD;

import java.util.List;
import software.amazon.smithy.model.traits.InputTrait;
import software.amazon.smithy.model.traits.OutputTrait;
import software.amazon.smithy.python.codegen.GenerationContext;
import software.amazon.smithy.python.codegen.integrations.PythonIntegration;
import software.amazon.smithy.python.codegen.sections.*;
import software.amazon.smithy.python.codegen.writer.PythonWriter;
import software.amazon.smithy.utils.CodeInterceptor;
import software.amazon.smithy.utils.CodeSection;

public class AwsRstDocFileGenerator implements PythonIntegration {

@Override
public List<? extends CodeInterceptor<? extends CodeSection, PythonWriter>> interceptors(
GenerationContext context
) {
return List.of(
// We generate custom RST files for each member that we want to have
// its own page. This gives us much more fine-grained control of
// what gets generated than just using automodule or autoclass on
// the client would alone.
new OperationGenerationInterceptor(context),
new StructureGenerationInterceptor(context),
new ErrorGenerationInterceptor(context),
new UnionGenerationInterceptor(context),
new UnionMemberGenerationInterceptor(context));
}

/**
* Utility method to generate a header for documentation files.
*
* @param title The title of the section.
* @return A formatted header string.
*/
private static String generateHeader(String title) {
return String.format("%s%n%s%n%n", title, "=".repeat(title.length()));
}

private static final class OperationGenerationInterceptor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the advantage of doing this over just autoclassing the client itself?

implements CodeInterceptor.Appender<OperationSection, PythonWriter> {

private final GenerationContext context;

public OperationGenerationInterceptor(GenerationContext context) {
this.context = context;
}

@Override
public Class<OperationSection> sectionType() {
return OperationSection.class;
}

@Override
public void append(PythonWriter pythonWriter, OperationSection section) {
var operation = section.operation();
var operationSymbol = context.symbolProvider().toSymbol(operation).expectProperty(OPERATION_METHOD);
var input = context.model().expectShape(operation.getInputShape());
var inputSymbol = context.symbolProvider().toSymbol(input);
var output = context.model().expectShape(operation.getOutputShape());
var outputSymbol = context.symbolProvider().toSymbol(output);

String operationName = operationSymbol.getName();
String inputSymbolName = inputSymbol.toString();
String outputSymbolName = outputSymbol.toString();
String serviceName = context.symbolProvider().toSymbol(section.service()).getName();
String docsFileName = String.format("docs/client/%s.rst", operationName);
String fullOperationReference = String.format("%s.client.%s.%s",
context.settings().moduleName(),
serviceName,
operationName);

context.writerDelegator().useFileWriter(docsFileName, "", fileWriter -> {
fileWriter.write(generateHeader(operationName));
fileWriter.write(".. automethod:: " + fullOperationReference + "\n\n");
fileWriter.write(".. toctree::\n :hidden:\n :maxdepth: 2\n\n");
fileWriter.write("=================\nInput:\n=================\n\n");
fileWriter.write(".. autoclass:: " + inputSymbolName + "\n :members:\n");
fileWriter.write("=================\nOutput:\n=================\n\n");
fileWriter.write(".. autoclass:: " + outputSymbolName + "\n :members:\n");
});
}
}

private static final class StructureGenerationInterceptor
implements CodeInterceptor.Appender<StructureSection, PythonWriter> {

private final GenerationContext context;

public StructureGenerationInterceptor(GenerationContext context) {
this.context = context;
}

@Override
public Class<StructureSection> sectionType() {
return StructureSection.class;
}

@Override
public void append(PythonWriter pythonWriter, StructureSection section) {
var shape = section.structure();
var symbol = context.symbolProvider().toSymbol(shape);
String docsFileName = String.format("docs/models/%s.rst",
symbol.getName());
if (!shape.hasTrait(InputTrait.class) && !shape.hasTrait(OutputTrait.class)) {
context.writerDelegator().useFileWriter(docsFileName, "", writer -> {
writer.write(generateHeader(symbol.getName()));
writer.write(".. autoclass:: " + symbol.toString() + "\n :members:\n");
});
}
}
}

private static final class ErrorGenerationInterceptor
implements CodeInterceptor.Appender<ErrorSection, PythonWriter> {

private final GenerationContext context;

public ErrorGenerationInterceptor(GenerationContext context) {
this.context = context;
}

@Override
public Class<ErrorSection> sectionType() {
return ErrorSection.class;
}

@Override
public void append(PythonWriter pythonWriter, ErrorSection section) {
var symbol = section.errorSymbol();
String docsFileName = String.format("docs/models/%s.rst",
symbol.getName());
context.writerDelegator().useFileWriter(docsFileName, "", writer -> {
writer.write(generateHeader(symbol.getName()));
writer.write(".. autoexception:: " + symbol.toString() + "\n :members:\n :show-inheritance:\n");
});
}
}

private static final class UnionGenerationInterceptor
implements CodeInterceptor.Appender<UnionSection, PythonWriter> {

private final GenerationContext context;

public UnionGenerationInterceptor(GenerationContext context) {
this.context = context;
}

@Override
public Class<UnionSection> sectionType() {
return UnionSection.class;
}

@Override
public void append(PythonWriter pythonWriter, UnionSection section) {
String parentName = section.parentName();
String docsFileName = String.format("docs/models/%s.rst", parentName);
context.writerDelegator().useFileWriter(docsFileName, "", writer -> {
writer.write(".. _" + parentName + ":\n\n");
writer.write(generateHeader(parentName));
writer.write(
".. autodata:: " + context.symbolProvider().toSymbol(section.unionShape()).toString() + " \n");
});
}
}

private static final class UnionMemberGenerationInterceptor
implements CodeInterceptor.Appender<UnionMemberSection, PythonWriter> {

private final GenerationContext context;

public UnionMemberGenerationInterceptor(GenerationContext context) {
this.context = context;
}

@Override
public Class<UnionMemberSection> sectionType() {
return UnionMemberSection.class;
}

@Override
public void append(PythonWriter pythonWriter, UnionMemberSection section) {
var memberSymbol = section.memberSymbol();
String symbolName = memberSymbol.getName();
String docsFileName = String.format("docs/models/%s.rst", symbolName);
context.writerDelegator().useFileWriter(docsFileName, "", writer -> {
writer.write(".. _" + symbolName + ":\n\n");
writer.write(generateHeader(symbolName));
writer.write(".. autoclass:: " + memberSymbol.toString() + " \n");
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ software.amazon.smithy.python.aws.codegen.AwsProtocolsIntegration
software.amazon.smithy.python.aws.codegen.AwsServiceIdIntegration
software.amazon.smithy.python.aws.codegen.AwsUserAgentIntegration
software.amazon.smithy.python.aws.codegen.AwsStandardRegionalEndpointsIntegration
software.amazon.smithy.python.aws.codegen.AwsRstDocFileGenerator
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.python.aws.codegen;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.python.codegen.writer.MarkdownToRstDocConverter;

public class MarkdownToRstDocConverterTest {

private MarkdownToRstDocConverter markdownToRstDocConverter;

@BeforeEach
public void setUp() {
markdownToRstDocConverter = MarkdownToRstDocConverter.getInstance();
}

@Test
public void testConvertCommonmarkToRstWithTitleAndParagraph() {
String html = "<html><body><h1>Title</h1><p>Paragraph</p></body></html>";
String expected = "\n\nTitle\n=====\nParagraph\n";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithImportantNote() {
String html = "<html><body><important>Important note</important></body></html>";
String expected = "\n\n.. important::\n Important note\n";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithList() {
String html = "<html><body><ul><li>Item 1</li><li>Item 2</li></ul></body></html>";
String expected = "\n\n* Item 1\n\n* Item 2\n\n";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithMixedElements() {
String html = "<html><body><h1>Title</h1><p>Paragraph</p><ul><li>Item 1</li><li>Item 2</li></ul></body></html>";
String expected = "\n\nTitle\n=====\nParagraph\n\n* Item 1\n\n* Item 2\n\n";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithNestedElements() {
String html = "<html><body><h1>Title</h1><p>Paragraph with <strong>bold</strong> text</p></body></html>";
String expected = "\n\nTitle\n=====\nParagraph with **bold** text\n";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithAnchorTag() {
String html = "<html><body><a href='https://example.com'>Link</a></body></html>";
String expected = "\n`Link <https://example.com>`_";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithBoldTag() {
String html = "<html><body><b>Bold text</b></body></html>";
String expected = "\n**Bold text**";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithItalicTag() {
String html = "<html><body><i>Italic text</i></body></html>";
String expected = "\n*Italic text*";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithCodeTag() {
String html = "<html><body><code>code snippet</code></body></html>";
String expected = "\n``code snippet``";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithNoteTag() {
String html = "<html><body><note>Note text</note></body></html>";
String expected = "\n\n.. note::\n Note text\n";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}

@Test
public void testConvertCommonmarkToRstWithNestedList() {
String html = "<html><body><ul><li>Item 1<ul><li>Subitem 1</li></ul></li><li>Item 2</li></ul></body></html>";
String expected = "\n\n* Item 1\n * Subitem 1\n\n* Item 2\n\n";
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
assertEquals(expected, result);
}
}
2 changes: 2 additions & 0 deletions codegen/core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ dependencies {
implementation(libs.smithy.protocol.test.traits)
// We have this because we're using RestJson1 as a 'generic' protocol.
implementation(libs.smithy.aws.traits)
implementation(libs.jsoup)
implementation(libs.commonmark)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@
import software.amazon.smithy.model.traits.StringTrait;
import software.amazon.smithy.python.codegen.integrations.PythonIntegration;
import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin;
import software.amazon.smithy.python.codegen.sections.InitializeHttpAuthParametersSection;
import software.amazon.smithy.python.codegen.sections.ResolveEndpointSection;
import software.amazon.smithy.python.codegen.sections.ResolveIdentitySection;
import software.amazon.smithy.python.codegen.sections.SendRequestSection;
import software.amazon.smithy.python.codegen.sections.SignRequestSection;
import software.amazon.smithy.python.codegen.sections.*;
import software.amazon.smithy.python.codegen.writer.PythonWriter;
import software.amazon.smithy.utils.SmithyInternalApi;

Expand Down Expand Up @@ -69,10 +65,10 @@ private void generateService(PythonWriter writer) {
$L

:param config: Optional configuration for the client. Here you can set things like the
endpoint for HTTP services or auth credentials.
endpoint for HTTP services or auth credentials.

:param plugins: A list of callables that modify the configuration dynamically. These
can be used to set defaults, for example.""", docs);
can be used to set defaults, for example.""", docs);
});

var defaultPlugins = new LinkedHashSet<SymbolReference>();
Expand Down Expand Up @@ -797,6 +793,7 @@ private void generateOperation(PythonWriter writer, OperationShape operation) {
var output = model.expectShape(operation.getOutputShape());
var outputSymbol = symbolProvider.toSymbol(output);

writer.pushState(new OperationSection(service, operation));
writer.openBlock("async def $L(self, input: $T, plugins: list[$T] | None = None) -> $T:",
"",
operationMethodSymbol.getName(),
Expand Down Expand Up @@ -824,26 +821,29 @@ private void generateOperation(PythonWriter writer, OperationShape operation) {
""", serSymbol, deserSymbol, operation.getId().getName());
}
});
writer.popState();
}

private void writeSharedOperationInit(PythonWriter writer, OperationShape operation, Shape input) {
writer.writeDocs(() -> {
var docs = operation.getTrait(DocumentationTrait.class)
var docs = writer.formatDocs(operation.getTrait(DocumentationTrait.class)
.map(StringTrait::getValue)
.orElse(String.format("Invokes the %s operation.", operation.getId().getName()));
.orElse(String.format("Invokes the %s operation.",
operation.getId().getName())));

var inputDocs = input.getTrait(DocumentationTrait.class)
.map(StringTrait::getValue)
.orElse("The operation's input.");

writer.write("""
$L

:param input: $L

:param plugins: A list of callables that modify the configuration dynamically.
Changes made by these plugins only apply for the duration of the operation
execution and will not affect any other operation invocations.""", docs, inputDocs);
Changes made by these plugins only apply for the duration of the operation
execution and will not affect any other operation invocations.

$L
""", inputDocs, docs);
});

var defaultPlugins = new LinkedHashSet<SymbolReference>();
Expand Down
Loading