Skip to content

Commit 6e4f19d

Browse files
committed
Fix OpenAPI example generation for documents
1 parent f30a514 commit 6e4f19d

3 files changed

Lines changed: 196 additions & 2 deletions

File tree

smithy-openapi/src/main/java/software/amazon/smithy/openapi/fromsmithy/protocols/JsonValueNodeTransformer.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
import software.amazon.smithy.openapi.fromsmithy.Context;
2121

2222
/**
23-
* Applies the jsonName trait to a node value if applicable.
23+
* Applies the jsonName trait to example node values if applicable, recursing through list elements,
24+
* map values, and structure/union members. Document-typed shapes pass through without transformation,
25+
* since documents accept any node value.
2426
*/
2527
public class JsonValueNodeTransformer implements NodeVisitor<Node> {
2628
private final Context<?> context;
@@ -59,11 +61,16 @@ public Node stringNode(StringNode node) {
5961

6062
@Override
6163
public Node arrayNode(ArrayNode node) {
62-
ArrayNode.Builder resultBuilder = ArrayNode.builder();
6364
Shape listShape = shape.asMemberShape()
6465
.map(m -> context.getModel().expectShape(m.getTarget()))
6566
.orElse(shape);
6667

68+
// Documents accept any node value, so short-circuit without trying to resolve a list element shape.
69+
if (listShape.isDocumentShape()) {
70+
return node;
71+
}
72+
73+
ArrayNode.Builder resultBuilder = ArrayNode.builder();
6774
Shape target = context.getModel().expectShape(listShape.asListShape().get().getMember().getTarget());
6875
JsonValueNodeTransformer elementTransformer = new JsonValueNodeTransformer(context, target);
6976
for (Node element : node.getElements()) {
@@ -78,6 +85,11 @@ public Node objectNode(ObjectNode node) {
7885
.map(m -> context.getModel().expectShape(m.getTarget()))
7986
.orElse(shape);
8087

88+
// Documents accept any node value, so short-circuit without trying to resolve member shapes.
89+
if (actual.isDocumentShape()) {
90+
return node;
91+
}
92+
8193
if (shape.isMapShape()) {
8294
return mapNode(actual.asMapShape().get(), node);
8395
}

smithy-openapi/src/test/java/software/amazon/smithy/openapi/fromsmithy/protocols/AwsRestJson1ProtocolTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.junit.jupiter.params.provider.ValueSource;
1818
import software.amazon.smithy.aws.traits.protocols.RestJson1Trait;
1919
import software.amazon.smithy.model.Model;
20+
import software.amazon.smithy.model.node.ArrayNode;
2021
import software.amazon.smithy.model.node.Node;
2122
import software.amazon.smithy.model.node.ObjectNode;
2223
import software.amazon.smithy.model.shapes.OperationShape;
@@ -282,6 +283,58 @@ public void convertsExamples() {
282283
}
283284
}
284285

286+
@ParameterizedTest
287+
@MethodSource("documentExampleCases")
288+
public void convertsExamplesWithDocumentMembers(String path, String exampleId, Node expectedValue) {
289+
Model model = Model.assembler()
290+
.addImport(getClass().getResource("document-examples-test.smithy"))
291+
.discoverModels()
292+
.assemble()
293+
.unwrap();
294+
OpenApiConfig config = new OpenApiConfig();
295+
config.setService(ShapeId.from("smithy.example.documentexamples#DocumentExamples"));
296+
ObjectNode result = OpenApiConverter.create()
297+
.config(config)
298+
.convertToNode(model);
299+
300+
// Navigate paths.<path>.get.responses["200"].content["application/json"].examples.<exampleId>.value.data
301+
Node actualValue = result.expectObjectMember("paths")
302+
.expectObjectMember(path)
303+
.expectObjectMember("get")
304+
.expectObjectMember("responses")
305+
.expectObjectMember("200")
306+
.expectObjectMember("content")
307+
.expectObjectMember("application/json")
308+
.expectObjectMember("examples")
309+
.expectObjectMember(exampleId)
310+
.expectObjectMember("value")
311+
.expectMember("data");
312+
313+
Node.assertEquals(actualValue, expectedValue);
314+
}
315+
316+
private static Stream<Arguments> documentExampleCases() {
317+
Node arrayValue = ArrayNode.builder()
318+
.withValue(ObjectNode.builder().withMember("name", "first").withMember("size", 1).build())
319+
.withValue(ObjectNode.builder().withMember("name", "second").withMember("size", 2).build())
320+
.build();
321+
Node objectValue = ObjectNode.builder()
322+
.withMember("name", "only")
323+
.withMember("size", 1)
324+
.build();
325+
Node scalarValue = Node.from("just-a-string");
326+
Node booleanValue = Node.from(true);
327+
Node numberValue = Node.from(42);
328+
Node nullValue = Node.nullNode();
329+
return Stream.of(
330+
Arguments.of("/array", "GetArrayDocument_example1", arrayValue),
331+
Arguments.of("/object", "GetObjectDocument_example1", objectValue),
332+
Arguments.of("/scalar", "GetScalarDocument_example1", scalarValue),
333+
Arguments.of("/boolean", "GetBooleanDocument_example1", booleanValue),
334+
Arguments.of("/number", "GetNumberDocument_example1", numberValue),
335+
Arguments.of("/null", "GetNullDocument_example1", nullValue));
336+
}
337+
285338
@Test
286339
public void combinesErrorsWithSameStatusCode() {
287340
Model model = Model.assembler()
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
$version: "2"
2+
3+
namespace smithy.example.documentexamples
4+
5+
use aws.protocols#restJson1
6+
7+
@restJson1
8+
service DocumentExamples {
9+
version: "2024-01-01"
10+
operations: [
11+
GetArrayDocument
12+
GetObjectDocument
13+
GetScalarDocument
14+
GetBooleanDocument
15+
GetNumberDocument
16+
GetNullDocument
17+
]
18+
}
19+
20+
@readonly
21+
@http(method: "GET", uri: "/array", code: 200)
22+
operation GetArrayDocument {
23+
output := {
24+
data: Document
25+
}
26+
}
27+
28+
@readonly
29+
@http(method: "GET", uri: "/object", code: 200)
30+
operation GetObjectDocument {
31+
output := {
32+
data: Document
33+
}
34+
}
35+
36+
@readonly
37+
@http(method: "GET", uri: "/scalar", code: 200)
38+
operation GetScalarDocument {
39+
output := {
40+
data: Document
41+
}
42+
}
43+
44+
@readonly
45+
@http(method: "GET", uri: "/boolean", code: 200)
46+
operation GetBooleanDocument {
47+
output := {
48+
data: Document
49+
}
50+
}
51+
52+
@readonly
53+
@http(method: "GET", uri: "/number", code: 200)
54+
operation GetNumberDocument {
55+
output := {
56+
data: Document
57+
}
58+
}
59+
60+
@readonly
61+
@http(method: "GET", uri: "/null", code: 200)
62+
operation GetNullDocument {
63+
output := {
64+
data: Document
65+
}
66+
}
67+
68+
apply GetArrayDocument @examples([
69+
{
70+
title: "Array-valued document"
71+
documentation: "Document member whose example value is a JSON array"
72+
output: {
73+
data: [
74+
{ name: "first", size: 1 }
75+
{ name: "second", size: 2 }
76+
]
77+
}
78+
}
79+
])
80+
81+
apply GetObjectDocument @examples([
82+
{
83+
title: "Object-valued document"
84+
documentation: "Document member whose example value is a JSON object"
85+
output: {
86+
data: { name: "only", size: 1 }
87+
}
88+
}
89+
])
90+
91+
apply GetScalarDocument @examples([
92+
{
93+
title: "Scalar-valued document"
94+
documentation: "Document member whose example value is a JSON string"
95+
output: {
96+
data: "just-a-string"
97+
}
98+
}
99+
])
100+
101+
apply GetBooleanDocument @examples([
102+
{
103+
title: "Boolean-valued document"
104+
documentation: "Document member whose example value is a JSON boolean"
105+
output: {
106+
data: true
107+
}
108+
}
109+
])
110+
111+
apply GetNumberDocument @examples([
112+
{
113+
title: "Number-valued document"
114+
documentation: "Document member whose example value is a JSON number"
115+
output: {
116+
data: 42
117+
}
118+
}
119+
])
120+
121+
apply GetNullDocument @examples([
122+
{
123+
title: "Null-valued document"
124+
documentation: "Document member whose example value is JSON null"
125+
output: {
126+
data: null
127+
}
128+
}
129+
])

0 commit comments

Comments
 (0)