Skip to content

Commit ff78144

Browse files
authored
Merge pull request smithy-lang#418 from SamRemis/convert-docs-to-rst
Add sphinx doc generation
2 parents 9c80e0a + 25f2bd4 commit ff78144

File tree

19 files changed

+951
-42
lines changed

19 files changed

+951
-42
lines changed
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.python.aws.codegen;
6+
7+
import static software.amazon.smithy.python.codegen.SymbolProperties.OPERATION_METHOD;
8+
9+
import java.util.List;
10+
import software.amazon.smithy.model.traits.InputTrait;
11+
import software.amazon.smithy.model.traits.OutputTrait;
12+
import software.amazon.smithy.python.codegen.GenerationContext;
13+
import software.amazon.smithy.python.codegen.integrations.PythonIntegration;
14+
import software.amazon.smithy.python.codegen.sections.*;
15+
import software.amazon.smithy.python.codegen.writer.PythonWriter;
16+
import software.amazon.smithy.utils.CodeInterceptor;
17+
import software.amazon.smithy.utils.CodeSection;
18+
19+
public class AwsRstDocFileGenerator implements PythonIntegration {
20+
21+
@Override
22+
public List<? extends CodeInterceptor<? extends CodeSection, PythonWriter>> interceptors(
23+
GenerationContext context
24+
) {
25+
return List.of(
26+
// We generate custom RST files for each member that we want to have
27+
// its own page. This gives us much more fine-grained control of
28+
// what gets generated than just using automodule or autoclass on
29+
// the client would alone.
30+
new OperationGenerationInterceptor(context),
31+
new StructureGenerationInterceptor(context),
32+
new ErrorGenerationInterceptor(context),
33+
new UnionGenerationInterceptor(context),
34+
new UnionMemberGenerationInterceptor(context));
35+
}
36+
37+
/**
38+
* Utility method to generate a header for documentation files.
39+
*
40+
* @param title The title of the section.
41+
* @return A formatted header string.
42+
*/
43+
private static String generateHeader(String title) {
44+
return String.format("%s%n%s%n%n", title, "=".repeat(title.length()));
45+
}
46+
47+
private static final class OperationGenerationInterceptor
48+
implements CodeInterceptor.Appender<OperationSection, PythonWriter> {
49+
50+
private final GenerationContext context;
51+
52+
public OperationGenerationInterceptor(GenerationContext context) {
53+
this.context = context;
54+
}
55+
56+
@Override
57+
public Class<OperationSection> sectionType() {
58+
return OperationSection.class;
59+
}
60+
61+
@Override
62+
public void append(PythonWriter pythonWriter, OperationSection section) {
63+
var operation = section.operation();
64+
var operationSymbol = context.symbolProvider().toSymbol(operation).expectProperty(OPERATION_METHOD);
65+
var input = context.model().expectShape(operation.getInputShape());
66+
var inputSymbol = context.symbolProvider().toSymbol(input);
67+
var output = context.model().expectShape(operation.getOutputShape());
68+
var outputSymbol = context.symbolProvider().toSymbol(output);
69+
70+
String operationName = operationSymbol.getName();
71+
String inputSymbolName = inputSymbol.toString();
72+
String outputSymbolName = outputSymbol.toString();
73+
String serviceName = context.symbolProvider().toSymbol(section.service()).getName();
74+
String docsFileName = String.format("docs/client/%s.rst", operationName);
75+
String fullOperationReference = String.format("%s.client.%s.%s",
76+
context.settings().moduleName(),
77+
serviceName,
78+
operationName);
79+
80+
context.writerDelegator().useFileWriter(docsFileName, "", fileWriter -> {
81+
fileWriter.write(generateHeader(operationName));
82+
fileWriter.write(".. automethod:: " + fullOperationReference + "\n\n");
83+
fileWriter.write(".. toctree::\n :hidden:\n :maxdepth: 2\n\n");
84+
fileWriter.write("=================\nInput:\n=================\n\n");
85+
fileWriter.write(".. autoclass:: " + inputSymbolName + "\n :members:\n");
86+
fileWriter.write("=================\nOutput:\n=================\n\n");
87+
fileWriter.write(".. autoclass:: " + outputSymbolName + "\n :members:\n");
88+
});
89+
}
90+
}
91+
92+
private static final class StructureGenerationInterceptor
93+
implements CodeInterceptor.Appender<StructureSection, PythonWriter> {
94+
95+
private final GenerationContext context;
96+
97+
public StructureGenerationInterceptor(GenerationContext context) {
98+
this.context = context;
99+
}
100+
101+
@Override
102+
public Class<StructureSection> sectionType() {
103+
return StructureSection.class;
104+
}
105+
106+
@Override
107+
public void append(PythonWriter pythonWriter, StructureSection section) {
108+
var shape = section.structure();
109+
var symbol = context.symbolProvider().toSymbol(shape);
110+
String docsFileName = String.format("docs/models/%s.rst",
111+
symbol.getName());
112+
if (!shape.hasTrait(InputTrait.class) && !shape.hasTrait(OutputTrait.class)) {
113+
context.writerDelegator().useFileWriter(docsFileName, "", writer -> {
114+
writer.write(generateHeader(symbol.getName()));
115+
writer.write(".. autoclass:: " + symbol.toString() + "\n :members:\n");
116+
});
117+
}
118+
}
119+
}
120+
121+
private static final class ErrorGenerationInterceptor
122+
implements CodeInterceptor.Appender<ErrorSection, PythonWriter> {
123+
124+
private final GenerationContext context;
125+
126+
public ErrorGenerationInterceptor(GenerationContext context) {
127+
this.context = context;
128+
}
129+
130+
@Override
131+
public Class<ErrorSection> sectionType() {
132+
return ErrorSection.class;
133+
}
134+
135+
@Override
136+
public void append(PythonWriter pythonWriter, ErrorSection section) {
137+
var symbol = section.errorSymbol();
138+
String docsFileName = String.format("docs/models/%s.rst",
139+
symbol.getName());
140+
context.writerDelegator().useFileWriter(docsFileName, "", writer -> {
141+
writer.write(generateHeader(symbol.getName()));
142+
writer.write(".. autoexception:: " + symbol.toString() + "\n :members:\n :show-inheritance:\n");
143+
});
144+
}
145+
}
146+
147+
private static final class UnionGenerationInterceptor
148+
implements CodeInterceptor.Appender<UnionSection, PythonWriter> {
149+
150+
private final GenerationContext context;
151+
152+
public UnionGenerationInterceptor(GenerationContext context) {
153+
this.context = context;
154+
}
155+
156+
@Override
157+
public Class<UnionSection> sectionType() {
158+
return UnionSection.class;
159+
}
160+
161+
@Override
162+
public void append(PythonWriter pythonWriter, UnionSection section) {
163+
String parentName = section.parentName();
164+
String docsFileName = String.format("docs/models/%s.rst", parentName);
165+
context.writerDelegator().useFileWriter(docsFileName, "", writer -> {
166+
writer.write(".. _" + parentName + ":\n\n");
167+
writer.write(generateHeader(parentName));
168+
writer.write(
169+
".. autodata:: " + context.symbolProvider().toSymbol(section.unionShape()).toString() + " \n");
170+
});
171+
}
172+
}
173+
174+
private static final class UnionMemberGenerationInterceptor
175+
implements CodeInterceptor.Appender<UnionMemberSection, PythonWriter> {
176+
177+
private final GenerationContext context;
178+
179+
public UnionMemberGenerationInterceptor(GenerationContext context) {
180+
this.context = context;
181+
}
182+
183+
@Override
184+
public Class<UnionMemberSection> sectionType() {
185+
return UnionMemberSection.class;
186+
}
187+
188+
@Override
189+
public void append(PythonWriter pythonWriter, UnionMemberSection section) {
190+
var memberSymbol = section.memberSymbol();
191+
String symbolName = memberSymbol.getName();
192+
String docsFileName = String.format("docs/models/%s.rst", symbolName);
193+
context.writerDelegator().useFileWriter(docsFileName, "", writer -> {
194+
writer.write(".. _" + symbolName + ":\n\n");
195+
writer.write(generateHeader(symbolName));
196+
writer.write(".. autoclass:: " + memberSymbol.toString() + " \n");
197+
});
198+
}
199+
}
200+
}

codegen/aws/core/src/main/resources/META-INF/services/software.amazon.smithy.python.codegen.integrations.PythonIntegration

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ software.amazon.smithy.python.aws.codegen.AwsProtocolsIntegration
88
software.amazon.smithy.python.aws.codegen.AwsServiceIdIntegration
99
software.amazon.smithy.python.aws.codegen.AwsUserAgentIntegration
1010
software.amazon.smithy.python.aws.codegen.AwsStandardRegionalEndpointsIntegration
11+
software.amazon.smithy.python.aws.codegen.AwsRstDocFileGenerator
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.python.aws.codegen;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import software.amazon.smithy.python.codegen.writer.MarkdownToRstDocConverter;
12+
13+
public class MarkdownToRstDocConverterTest {
14+
15+
private MarkdownToRstDocConverter markdownToRstDocConverter;
16+
17+
@BeforeEach
18+
public void setUp() {
19+
markdownToRstDocConverter = MarkdownToRstDocConverter.getInstance();
20+
}
21+
22+
@Test
23+
public void testConvertCommonmarkToRstWithTitleAndParagraph() {
24+
String html = "<html><body><h1>Title</h1><p>Paragraph</p></body></html>";
25+
String expected = "\n\nTitle\n=====\nParagraph\n";
26+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
27+
assertEquals(expected, result);
28+
}
29+
30+
@Test
31+
public void testConvertCommonmarkToRstWithImportantNote() {
32+
String html = "<html><body><important>Important note</important></body></html>";
33+
String expected = "\n\n.. important::\n Important note\n";
34+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
35+
assertEquals(expected, result);
36+
}
37+
38+
@Test
39+
public void testConvertCommonmarkToRstWithList() {
40+
String html = "<html><body><ul><li>Item 1</li><li>Item 2</li></ul></body></html>";
41+
String expected = "\n\n* Item 1\n\n* Item 2\n\n";
42+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
43+
assertEquals(expected, result);
44+
}
45+
46+
@Test
47+
public void testConvertCommonmarkToRstWithMixedElements() {
48+
String html = "<html><body><h1>Title</h1><p>Paragraph</p><ul><li>Item 1</li><li>Item 2</li></ul></body></html>";
49+
String expected = "\n\nTitle\n=====\nParagraph\n\n* Item 1\n\n* Item 2\n\n";
50+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
51+
assertEquals(expected, result);
52+
}
53+
54+
@Test
55+
public void testConvertCommonmarkToRstWithNestedElements() {
56+
String html = "<html><body><h1>Title</h1><p>Paragraph with <strong>bold</strong> text</p></body></html>";
57+
String expected = "\n\nTitle\n=====\nParagraph with **bold** text\n";
58+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
59+
assertEquals(expected, result);
60+
}
61+
62+
@Test
63+
public void testConvertCommonmarkToRstWithAnchorTag() {
64+
String html = "<html><body><a href='https://example.com'>Link</a></body></html>";
65+
String expected = "\n`Link <https://example.com>`_";
66+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
67+
assertEquals(expected, result);
68+
}
69+
70+
@Test
71+
public void testConvertCommonmarkToRstWithBoldTag() {
72+
String html = "<html><body><b>Bold text</b></body></html>";
73+
String expected = "\n**Bold text**";
74+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
75+
assertEquals(expected, result);
76+
}
77+
78+
@Test
79+
public void testConvertCommonmarkToRstWithItalicTag() {
80+
String html = "<html><body><i>Italic text</i></body></html>";
81+
String expected = "\n*Italic text*";
82+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
83+
assertEquals(expected, result);
84+
}
85+
86+
@Test
87+
public void testConvertCommonmarkToRstWithCodeTag() {
88+
String html = "<html><body><code>code snippet</code></body></html>";
89+
String expected = "\n``code snippet``";
90+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
91+
assertEquals(expected, result);
92+
}
93+
94+
@Test
95+
public void testConvertCommonmarkToRstWithNoteTag() {
96+
String html = "<html><body><note>Note text</note></body></html>";
97+
String expected = "\n\n.. note::\n Note text\n";
98+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
99+
assertEquals(expected, result);
100+
}
101+
102+
@Test
103+
public void testConvertCommonmarkToRstWithNestedList() {
104+
String html = "<html><body><ul><li>Item 1<ul><li>Subitem 1</li></ul></li><li>Item 2</li></ul></body></html>";
105+
String expected = "\n\n* Item 1\n * Subitem 1\n\n* Item 2\n\n";
106+
String result = markdownToRstDocConverter.convertCommonmarkToRst(html);
107+
assertEquals(expected, result);
108+
}
109+
}

codegen/core/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ dependencies {
1515
implementation(libs.smithy.protocol.test.traits)
1616
// We have this because we're using RestJson1 as a 'generic' protocol.
1717
implementation(libs.smithy.aws.traits)
18+
implementation(libs.jsoup)
19+
implementation(libs.commonmark)
1820
}

codegen/core/src/main/java/software/amazon/smithy/python/codegen/ClientGenerator.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,7 @@
2323
import software.amazon.smithy.model.traits.StringTrait;
2424
import software.amazon.smithy.python.codegen.integrations.PythonIntegration;
2525
import software.amazon.smithy.python.codegen.integrations.RuntimeClientPlugin;
26-
import software.amazon.smithy.python.codegen.sections.InitializeHttpAuthParametersSection;
27-
import software.amazon.smithy.python.codegen.sections.ResolveEndpointSection;
28-
import software.amazon.smithy.python.codegen.sections.ResolveIdentitySection;
29-
import software.amazon.smithy.python.codegen.sections.SendRequestSection;
30-
import software.amazon.smithy.python.codegen.sections.SignRequestSection;
26+
import software.amazon.smithy.python.codegen.sections.*;
3127
import software.amazon.smithy.python.codegen.writer.PythonWriter;
3228
import software.amazon.smithy.utils.SmithyInternalApi;
3329

@@ -69,10 +65,10 @@ private void generateService(PythonWriter writer) {
6965
$L
7066
7167
:param config: Optional configuration for the client. Here you can set things like the
72-
endpoint for HTTP services or auth credentials.
68+
endpoint for HTTP services or auth credentials.
7369
7470
:param plugins: A list of callables that modify the configuration dynamically. These
75-
can be used to set defaults, for example.""", docs);
71+
can be used to set defaults, for example.""", docs);
7672
});
7773

7874
var defaultPlugins = new LinkedHashSet<SymbolReference>();
@@ -827,6 +823,7 @@ private void generateOperation(PythonWriter writer, OperationShape operation) {
827823
var output = model.expectShape(operation.getOutputShape());
828824
var outputSymbol = symbolProvider.toSymbol(output);
829825

826+
writer.pushState(new OperationSection(service, operation));
830827
writer.openBlock("async def $L(self, input: $T, plugins: list[$T] | None = None) -> $T:",
831828
"",
832829
operationMethodSymbol.getName(),
@@ -854,26 +851,29 @@ private void generateOperation(PythonWriter writer, OperationShape operation) {
854851
""", serSymbol, deserSymbol, operationSymbol);
855852
}
856853
});
854+
writer.popState();
857855
}
858856

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

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

869868
writer.write("""
870-
$L
871-
872869
:param input: $L
873870
874871
:param plugins: A list of callables that modify the configuration dynamically.
875-
Changes made by these plugins only apply for the duration of the operation
876-
execution and will not affect any other operation invocations.""", docs, inputDocs);
872+
Changes made by these plugins only apply for the duration of the operation
873+
execution and will not affect any other operation invocations.
874+
875+
$L
876+
""", inputDocs, docs);
877877
});
878878

879879
var defaultPlugins = new LinkedHashSet<SymbolReference>();

0 commit comments

Comments
 (0)