Skip to content

Commit ad54ad7

Browse files
Generate examples
This adds support for the examples trait to javadoc generation for clients.
1 parent 0bbd00e commit ad54ad7

File tree

7 files changed

+365
-0
lines changed

7 files changed

+365
-0
lines changed

codegen/codegen-core/src/main/java/software/amazon/smithy/java/codegen/writer/JavaWriter.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ private void addImport(Symbol symbol) {
5959
addImport(symbol, symbol.getName());
6060
}
6161

62+
/**
63+
* @return Returns the namespace that the file being written is contained within.
64+
*/
65+
public String getPackageNamespace() {
66+
return packageNamespace;
67+
}
68+
69+
/**
70+
* @return Returns the name of the file being written to.
71+
*/
72+
public String getFilename() {
73+
return filename;
74+
}
75+
6276
@Override
6377
public String toString() {
6478
// Do not add headers or attempt symbol resolution for resource files
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.codegen.client.integrations.javadoc;
7+
8+
import java.util.List;
9+
import software.amazon.smithy.java.codegen.CodeGenerationContext;
10+
import software.amazon.smithy.java.codegen.JavaCodegenIntegration;
11+
import software.amazon.smithy.java.codegen.writer.JavaWriter;
12+
import software.amazon.smithy.utils.CodeInterceptor;
13+
import software.amazon.smithy.utils.CodeSection;
14+
import software.amazon.smithy.utils.SmithyInternalApi;
15+
16+
/**
17+
* Adds client examples to the generated Javadoc.
18+
*/
19+
@SmithyInternalApi
20+
public class ClientJavadocExamplesIntegration implements JavaCodegenIntegration {
21+
22+
@Override
23+
public String name() {
24+
return "client-javadoc-examples";
25+
}
26+
27+
@Override
28+
public List<String> runBefore() {
29+
// The DocumentationTrait interceptor uses "prepend", and the finalizing formatter
30+
// wholly replaces the contents of the JavaDoc section with a formatted version.
31+
// By running before the "javadoc" plugin, which includes those interceptors,
32+
// we can be sure that what we write will appear after the docs from the doc trait
33+
// and will still benefit from formatting.
34+
return List.of("javadoc");
35+
}
36+
37+
@Override
38+
public List<? extends CodeInterceptor<? extends CodeSection, JavaWriter>> interceptors(
39+
CodeGenerationContext codegenContext
40+
) {
41+
return List.of(new ExamplesTraitInterceptor(codegenContext));
42+
}
43+
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.codegen.client.integrations.javadoc;
7+
8+
import java.nio.file.Paths;
9+
import software.amazon.smithy.codegen.core.CodegenException;
10+
import software.amazon.smithy.java.codegen.CodeGenerationContext;
11+
import software.amazon.smithy.java.codegen.CodegenUtils;
12+
import software.amazon.smithy.java.codegen.generators.SnippetGenerator;
13+
import software.amazon.smithy.java.codegen.sections.JavadocSection;
14+
import software.amazon.smithy.java.codegen.writer.JavaWriter;
15+
import software.amazon.smithy.model.shapes.OperationShape;
16+
import software.amazon.smithy.model.shapes.ServiceShape;
17+
import software.amazon.smithy.model.traits.ExamplesTrait;
18+
import software.amazon.smithy.utils.CodeInterceptor;
19+
import software.amazon.smithy.utils.StringUtils;
20+
21+
/**
22+
* Adds Javadoc examples to operations with the examples trait.
23+
*/
24+
public class ExamplesTraitInterceptor implements CodeInterceptor.Appender<JavadocSection, JavaWriter> {
25+
26+
private final CodeGenerationContext context;
27+
28+
public ExamplesTraitInterceptor(CodeGenerationContext context) {
29+
this.context = context;
30+
}
31+
32+
@Override
33+
public void append(JavaWriter writer, JavadocSection section) {
34+
var operation = section.targetedShape()
35+
.asOperationShape()
36+
.orElseThrow(() -> new CodegenException(String.format(
37+
"Expected shape to be an operation shape, but was " + section.targetedShape().getType())));
38+
var trait = section.targetedShape().expectTrait(ExamplesTrait.class);
39+
writer.pushState();
40+
41+
var operationSymbol = context.symbolProvider().toSymbol(operation);
42+
43+
// The effective heading levels are different if the documentation is being put
44+
// in the operation's generated class docs or the client's generated method docs,
45+
// so this checks to see which file we're in and adjusts the heading level
46+
// accordingly.
47+
var operationFile = Paths.get(operationSymbol.getDefinitionFile()).normalize();
48+
var activeFile = Paths.get(writer.getFilename()).normalize();
49+
if (operationFile.equals(activeFile)) {
50+
writer.putContext("sectionHeading", "h2");
51+
writer.putContext("titleHeading", "h3");
52+
} else {
53+
writer.putContext("sectionHeading", "h4");
54+
writer.putContext("titleHeading", "h5");
55+
}
56+
57+
writer.write("<${sectionHeading:L}>Examples</${sectionHeading:L}>");
58+
for (ExamplesTrait.Example example : trait.getExamples()) {
59+
writer.pushState();
60+
writer.putContext("docs", example.getDocumentation().orElse(null));
61+
writer.putContext("title", example.getTitle());
62+
writer.write("""
63+
<${titleHeading:L}>${title:L}</${titleHeading:L}>
64+
65+
${?docs}<p>${docs:L}
66+
${/docs}
67+
<pre>
68+
{@code
69+
${C|}
70+
}
71+
</pre>
72+
""", writer.consumer(w -> writeExampleSnippet(writer, operation, example)));
73+
writer.popState();
74+
}
75+
writer.popState();
76+
}
77+
78+
// TODO: collect these and write them out to a shared snippets file
79+
private void writeExampleSnippet(JavaWriter writer, OperationShape operation, ExamplesTrait.Example example) {
80+
var service = context.model().expectShape(context.settings().service(), ServiceShape.class);
81+
var operationName = StringUtils.uncapitalize(CodegenUtils.getDefaultName(operation, service));
82+
writer.putContext("operationName", operationName);
83+
84+
var inputShape = context.model().expectShape(operation.getInputShape());
85+
writer.putContext(
86+
"input",
87+
SnippetGenerator.generateShapeInitializer(context, inputShape, example.getInput()));
88+
89+
if (example.getOutput().isPresent() && !example.getOutput().get().isEmpty()) {
90+
var outputShape = context.model().expectShape(operation.getOutputShape());
91+
writer.putContext(
92+
"output",
93+
SnippetGenerator.generateShapeInitializer(context, outputShape, example.getOutput().get()));
94+
} else {
95+
writer.putContext("output", null);
96+
}
97+
98+
if (example.getError().isPresent()) {
99+
writer.putContext("hasError", true);
100+
var error = example.getError().get();
101+
var errorShape = context.model().expectShape(error.getShapeId());
102+
writer.putContext(
103+
"error",
104+
SnippetGenerator.generateShapeInitializer(context, errorShape, error.getContent()));
105+
writer.putContext("errorSymbol", context.symbolProvider().toSymbol(errorShape));
106+
} else {
107+
writer.putContext("hasError", false);
108+
}
109+
110+
writer.writeInline("""
111+
var input = ${input:L|};
112+
${?hasError}
113+
114+
try {
115+
client.${operationName:L}(input);
116+
} catch (${errorSymbol:T} e) {
117+
e.equals(${error:L|});
118+
}
119+
${/hasError}
120+
${^hasError}
121+
122+
var result = client.${operationName:L}(input);
123+
result.equals(${output:L|});
124+
${/hasError}""");
125+
}
126+
127+
@Override
128+
public Class<JavadocSection> sectionType() {
129+
return JavadocSection.class;
130+
}
131+
132+
@Override
133+
public boolean isIntercepted(JavadocSection section) {
134+
// The examples trait can only be applied to operations for now,
135+
// but we add an explicit check anyway in case it ever gets expanded.
136+
var shape = section.targetedShape();
137+
return shape.hasTrait(ExamplesTrait.class) && shape.isOperationShape();
138+
}
139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
software.amazon.smithy.java.codegen.client.integrations.javadoc.ClientJavadocExamplesIntegration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.codegen.client.integrations.javadoc;
7+
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
import static org.hamcrest.Matchers.containsString;
10+
11+
import java.net.URL;
12+
import java.util.Objects;
13+
import org.junit.jupiter.api.Test;
14+
import software.amazon.smithy.java.codegen.client.utils.AbstractCodegenFileTest;
15+
16+
public class ClientJavadocExamplesIntegrationTest extends AbstractCodegenFileTest {
17+
private static final URL TEST_FILE = Objects.requireNonNull(
18+
ClientJavadocExamplesIntegrationTest.class.getResource("javadoc-examples.smithy"));
19+
20+
@Override
21+
protected URL testFile() {
22+
return TEST_FILE;
23+
}
24+
25+
@Test
26+
void includesExamples() {
27+
var fileContents = getFileStringForClass("client/TestServiceClient");
28+
var expected = """
29+
* <h4>Examples</h4>
30+
* <h5>Basic Example</h5>
31+
* <pre>
32+
* {@code
33+
* var input = ExamplesOperationInput.builder()
34+
* .foo("foo")
35+
* .build();
36+
*
37+
* var result = client.examplesOperation(input);
38+
* result.equals(ExamplesOperationOutput.builder()
39+
* .bar("bar")
40+
* .build());
41+
* }
42+
* </pre>
43+
*
44+
* <h5>Error Example</h5>
45+
* <pre>
46+
* {@code
47+
* var input = ExamplesOperationInput.builder()
48+
* .foo("bar")
49+
* .build();
50+
*
51+
* try {
52+
* client.examplesOperation(input);
53+
* } catch (ExampleError e) {
54+
* e.equals(ExampleError.builder()
55+
* .message("bar")
56+
* .build());
57+
* }
58+
* }
59+
* </pre>
60+
""";
61+
assertThat(fileContents, containsString(expected));
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.codegen.client.utils;
7+
8+
import static org.junit.jupiter.api.Assertions.assertFalse;
9+
import static org.junit.jupiter.api.Assertions.assertTrue;
10+
11+
import java.net.URL;
12+
import java.nio.file.Paths;
13+
import org.junit.jupiter.api.BeforeEach;
14+
import software.amazon.smithy.build.MockManifest;
15+
import software.amazon.smithy.build.PluginContext;
16+
import software.amazon.smithy.build.SmithyBuildPlugin;
17+
import software.amazon.smithy.java.codegen.client.JavaClientCodegenPlugin;
18+
import software.amazon.smithy.model.Model;
19+
import software.amazon.smithy.model.node.ObjectNode;
20+
21+
public abstract class AbstractCodegenFileTest {
22+
23+
protected final MockManifest manifest = new MockManifest();
24+
protected final SmithyBuildPlugin plugin = new JavaClientCodegenPlugin();
25+
26+
@BeforeEach
27+
public void setup() {
28+
var model = Model.assembler()
29+
.addImport(testFile())
30+
.assemble()
31+
.unwrap();
32+
var context = PluginContext.builder()
33+
.fileManifest(manifest)
34+
.settings(settings())
35+
.model(model)
36+
.build();
37+
plugin.execute(context);
38+
assertFalse(manifest.getFiles().isEmpty());
39+
}
40+
41+
protected abstract URL testFile();
42+
43+
protected ObjectNode settings() {
44+
return ObjectNode.builder()
45+
.withMember("service", "smithy.java.codegen#TestService")
46+
.withMember("namespace", "test.smithy.codegen")
47+
.build();
48+
}
49+
50+
protected String getFileStringForClass(String className) {
51+
var fileStringOptional = manifest.getFileString(
52+
Paths.get(String.format("/test/smithy/codegen/%s.java", className)));
53+
assertTrue(fileStringOptional.isPresent());
54+
return fileStringOptional.get();
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
$version: "2"
2+
3+
namespace smithy.java.codegen
4+
5+
service TestService {
6+
operations: [
7+
ExamplesOperation
8+
]
9+
}
10+
11+
@error("server")
12+
structure ExampleError {
13+
message: String
14+
}
15+
16+
/// Base docs
17+
@examples([
18+
{
19+
title: "Basic Example"
20+
input: {
21+
foo: "foo"
22+
}
23+
output: {
24+
bar: "bar"
25+
}
26+
}
27+
{
28+
title: "Error Example"
29+
input: {
30+
foo: "bar"
31+
}
32+
error: {
33+
shapeId: ExampleError
34+
content: {
35+
message: "bar"
36+
}
37+
}
38+
}
39+
])
40+
operation ExamplesOperation {
41+
input := {
42+
foo: String
43+
}
44+
output := {
45+
bar: String
46+
}
47+
errors: [ExampleError]
48+
}

0 commit comments

Comments
 (0)