Skip to content

Commit 691a904

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

File tree

6 files changed

+334
-0
lines changed

6 files changed

+334
-0
lines changed
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,122 @@
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 software.amazon.smithy.codegen.core.CodegenException;
9+
import software.amazon.smithy.java.codegen.CodeGenerationContext;
10+
import software.amazon.smithy.java.codegen.CodegenUtils;
11+
import software.amazon.smithy.java.codegen.generators.SnippetGenerator;
12+
import software.amazon.smithy.java.codegen.sections.JavadocSection;
13+
import software.amazon.smithy.java.codegen.writer.JavaWriter;
14+
import software.amazon.smithy.model.shapes.OperationShape;
15+
import software.amazon.smithy.model.shapes.ServiceShape;
16+
import software.amazon.smithy.model.traits.ExamplesTrait;
17+
import software.amazon.smithy.utils.CodeInterceptor;
18+
import software.amazon.smithy.utils.StringUtils;
19+
20+
/**
21+
* Adds Javadoc examples to operations with the examples trait.
22+
*/
23+
public class ExamplesTraitInterceptor implements CodeInterceptor.Appender<JavadocSection, JavaWriter> {
24+
25+
private final CodeGenerationContext context;
26+
27+
public ExamplesTraitInterceptor(CodeGenerationContext context) {
28+
this.context = context;
29+
}
30+
31+
@Override
32+
public void append(JavaWriter writer, JavadocSection section) {
33+
var operation = section.targetedShape()
34+
.asOperationShape()
35+
.orElseThrow(() -> new CodegenException(String.format(
36+
"Expected shape to be an operation shape, but was " + section.targetedShape().getType())));
37+
var trait = section.targetedShape().expectTrait(ExamplesTrait.class);
38+
writer.pushState();
39+
40+
writer.write("<h4>Examples</h4>");
41+
for (ExamplesTrait.Example example : trait.getExamples()) {
42+
writer.pushState();
43+
writer.putContext("docs", example.getDocumentation().orElse(null));
44+
writer.putContext("title", example.getTitle());
45+
writer.write("""
46+
<h5>${title:L}</h5>
47+
48+
${?docs}<p>${docs:L}
49+
${/docs}
50+
<pre>
51+
{@code
52+
${C|}
53+
}
54+
</pre>
55+
""", writer.consumer(w -> writeExampleSnippet(writer, operation, example)));
56+
writer.popState();
57+
}
58+
writer.popState();
59+
}
60+
61+
// TODO: collect these and write them out to a shared snippets file
62+
private void writeExampleSnippet(JavaWriter writer, OperationShape operation, ExamplesTrait.Example example) {
63+
var service = context.model().expectShape(context.settings().service(), ServiceShape.class);
64+
var operationName = StringUtils.uncapitalize(CodegenUtils.getDefaultName(operation, service));
65+
writer.putContext("operationName", operationName);
66+
67+
var inputShape = context.model().expectShape(operation.getInputShape());
68+
writer.putContext(
69+
"input",
70+
SnippetGenerator.generateShapeInitializer(context, inputShape, example.getInput()));
71+
72+
if (example.getOutput().isPresent() && !example.getOutput().get().isEmpty()) {
73+
var outputShape = context.model().expectShape(operation.getOutputShape());
74+
writer.putContext(
75+
"output",
76+
SnippetGenerator.generateShapeInitializer(context, outputShape, example.getOutput().get()));
77+
} else {
78+
writer.putContext("output", null);
79+
}
80+
81+
if (example.getError().isPresent()) {
82+
writer.putContext("hasError", true);
83+
var error = example.getError().get();
84+
var errorShape = context.model().expectShape(error.getShapeId());
85+
writer.putContext(
86+
"error",
87+
SnippetGenerator.generateShapeInitializer(context, errorShape, error.getContent()));
88+
writer.putContext("errorSymbol", context.symbolProvider().toSymbol(errorShape));
89+
} else {
90+
writer.putContext("hasError", false);
91+
}
92+
93+
writer.writeInline("""
94+
var input = ${input:L|};
95+
${?hasError}
96+
97+
try {
98+
client.${operationName:L}(input);
99+
} catch (${errorSymbol:T} e) {
100+
e.equals(${error:L|});
101+
}
102+
${/hasError}
103+
${^hasError}
104+
105+
var result = client.${operationName:L}(input);
106+
result.equals(${output:L|});
107+
${/hasError}""");
108+
}
109+
110+
@Override
111+
public Class<JavadocSection> sectionType() {
112+
return JavadocSection.class;
113+
}
114+
115+
@Override
116+
public boolean isIntercepted(JavadocSection section) {
117+
// The examples trait can only be applied to operations for now,
118+
// but we add an explicit check anyway in case it ever gets expanded.
119+
var shape = section.targetedShape();
120+
return shape.hasTrait(ExamplesTrait.class) && shape.isOperationShape();
121+
}
122+
}
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)