Skip to content

Commit 0ac2c72

Browse files
committed
add structural hint to command examples
1 parent 0d4eb34 commit 0ac2c72

File tree

3 files changed

+379
-0
lines changed

3 files changed

+379
-0
lines changed

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/CommandGenerator.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import software.amazon.smithy.model.traits.DocumentationTrait;
4343
import software.amazon.smithy.model.traits.ErrorTrait;
4444
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait;
45+
import software.amazon.smithy.typescript.codegen.documentation.StructureExampleGenerator;
4546
import software.amazon.smithy.typescript.codegen.endpointsV2.EndpointsParamNameMap;
4647
import software.amazon.smithy.typescript.codegen.endpointsV2.RuleSetParameterFinder;
4748
import software.amazon.smithy.typescript.codegen.integration.ProtocolGenerator;
@@ -174,6 +175,9 @@ private String getCommandExample(String serviceName, String configName, String c
174175
+ String.format("// const { %s, %s } = require(\"%s\"); // CommonJS import%n", serviceName, commandName,
175176
packageName)
176177
+ String.format("const client = new %s(config);%n", serviceName)
178+
+ String.format("const input = %s%n",
179+
StructureExampleGenerator.generateStructuralHintDocumentation(
180+
model.getShape(operation.getInputShape()).get(), model))
177181
+ String.format("const command = new %s(input);%n", commandName)
178182
+ "const response = await client.send(command);\n"
179183
+ "```\n"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
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.typescript.codegen.documentation;
7+
8+
import java.util.Arrays;
9+
import java.util.Collections;
10+
import java.util.HashMap;
11+
import java.util.HashSet;
12+
import java.util.Map;
13+
import java.util.Set;
14+
import java.util.stream.Collectors;
15+
import software.amazon.smithy.model.Model;
16+
import software.amazon.smithy.model.shapes.EnumShape;
17+
import software.amazon.smithy.model.shapes.IntEnumShape;
18+
import software.amazon.smithy.model.shapes.ListShape;
19+
import software.amazon.smithy.model.shapes.MapShape;
20+
import software.amazon.smithy.model.shapes.MemberShape;
21+
import software.amazon.smithy.model.shapes.Shape;
22+
import software.amazon.smithy.model.shapes.StructureShape;
23+
import software.amazon.smithy.model.shapes.UnionShape;
24+
import software.amazon.smithy.model.traits.RequiredTrait;
25+
import software.amazon.smithy.model.traits.StreamingTrait;
26+
27+
/**
28+
* Generates a structural hint for a shape used in command documentation.
29+
*/
30+
public abstract class StructureExampleGenerator {
31+
/**
32+
* Generates an example structure for API documentation, as an
33+
* automated gap filler for operations that do not have
34+
* hand written examples.
35+
*
36+
* Example for Athena::createPreparedStatement
37+
* ```js
38+
* const input = {
39+
* // QueryStatement: 'STRING_VALUE', // required
40+
* // StatementName: 'STRING_VALUE', // required
41+
* // WorkGroup: 'STRING_VALUE', // required
42+
* // Description: 'STRING_VALUE'
43+
* };
44+
* ```
45+
*/
46+
public static String generateStructuralHintDocumentation(Shape shape, Model model) {
47+
StringBuilder buffer = new StringBuilder();
48+
shape(shape, buffer, model, 0, new ShapeTracker());
49+
50+
// replace non-leading whitespace with single space.
51+
String s = Arrays.stream(
52+
buffer.toString()
53+
.split("\n"))
54+
.map(line -> line.replaceAll(
55+
"([\\w\\\",:] )\\s+",
56+
"$1"))
57+
.collect(Collectors.joining("\n"));
58+
59+
return s;
60+
}
61+
62+
private static void structure(StructureShape structureShape,
63+
StringBuilder buffer, Model model,
64+
int indentation,
65+
ShapeTracker shapeTracker) {
66+
if (structureShape.getAllMembers().size() == 0) {
67+
append(indentation, buffer, "{}");
68+
checkRequired(indentation, buffer, structureShape);
69+
} else {
70+
append(indentation, buffer, "{");
71+
checkRequired(indentation, buffer, structureShape);
72+
structureShape.getAllMembers().values().forEach(member -> {
73+
append(indentation + 2, buffer, member.getMemberName() + ": ");
74+
shape(member, buffer, model, indentation + 2, shapeTracker);
75+
});
76+
append(indentation, buffer, "}\n");
77+
}
78+
}
79+
80+
private static void union(UnionShape unionShape,
81+
StringBuilder buffer,
82+
Model model,
83+
int indentation,
84+
ShapeTracker shapeTracker) {
85+
append(indentation, buffer, "{ // Union: only one key present");
86+
checkRequired(indentation, buffer, unionShape);
87+
unionShape.getAllMembers().values().forEach(member -> {
88+
append(indentation + 2, buffer, member.getMemberName() + ": ");
89+
shape(member, buffer, model, indentation + 2, shapeTracker);
90+
});
91+
append(indentation, buffer, "}\n");
92+
}
93+
94+
private static void shape(Shape shape,
95+
StringBuilder buffer,
96+
Model model,
97+
int indentation,
98+
ShapeTracker shapeTracker) {
99+
Shape target;
100+
if (shape instanceof MemberShape) {
101+
target = model.getShape(((MemberShape) shape).getTarget()).get();
102+
} else {
103+
target = shape;
104+
}
105+
106+
shapeTracker.mark(shape, indentation);
107+
if (shapeTracker.getOccurrenceDepths(shape) > 5) {
108+
append(indentation, buffer, "\"<" + shape.getId().getName() + ">\"");
109+
} else {
110+
switch (target.getType()) {
111+
case BIG_DECIMAL:
112+
append(indentation, buffer, "Number('bigdecimal'),");
113+
break;
114+
case BIG_INTEGER:
115+
append(indentation, buffer, "Number('bigint'),");
116+
break;
117+
case BLOB:
118+
if (target.hasTrait(StreamingTrait.class)) {
119+
append(indentation, buffer, "\"STREAMING_BLOB_VALUE\",");
120+
} else {
121+
append(indentation, buffer, "\"BLOB_VALUE\",");
122+
}
123+
break;
124+
case BOOLEAN:
125+
append(indentation, buffer, "true || false,");
126+
break;
127+
case BYTE:
128+
append(indentation, buffer, "\"BYTE_VALUE\",");
129+
break;
130+
case DOCUMENT:
131+
append(indentation, buffer, "\"DOCUMENT_VALUE\",");
132+
break;
133+
case DOUBLE:
134+
append(indentation, buffer, "Number('double'),");
135+
break;
136+
case FLOAT:
137+
append(indentation, buffer, "Number('float'),");
138+
break;
139+
case INTEGER:
140+
append(indentation, buffer, "Number('int'),");
141+
break;
142+
case LONG:
143+
append(indentation, buffer, "Number('long'),");
144+
break;
145+
case SHORT:
146+
append(indentation, buffer, "Number('short'),");
147+
break;
148+
case STRING:
149+
append(indentation, buffer, "\"STRING_VALUE\",");
150+
break;
151+
case TIMESTAMP:
152+
append(indentation, buffer, "\"TIMESTAMP\",");
153+
break;
154+
155+
case SET:
156+
case LIST:
157+
append(indentation, buffer, "[");
158+
checkRequired(indentation, buffer, shape);
159+
ListShape list = (ListShape) target;
160+
shape(list.getMember(), buffer, model, indentation + 2, shapeTracker);
161+
append(indentation, buffer, "],\n");
162+
break;
163+
case MAP:
164+
append(indentation, buffer, "{");
165+
checkRequired(indentation, buffer, shape);
166+
append(indentation + 2, buffer, "\"<keys>\": ");
167+
MapShape map = (MapShape) target;
168+
shape(model.getShape(map.getValue().getTarget()).get(), buffer, model, indentation + 2,
169+
shapeTracker);
170+
append(indentation, buffer, "},\n");
171+
break;
172+
173+
case STRUCTURE:
174+
StructureShape structure = (StructureShape) target;
175+
structure(structure, buffer, model, indentation, shapeTracker);
176+
break;
177+
case UNION:
178+
UnionShape union = (UnionShape) target;
179+
union(union, buffer, model, indentation, shapeTracker);
180+
break;
181+
182+
case ENUM:
183+
EnumShape enumShape = (EnumShape) target;
184+
String enumeration = enumShape.getEnumValues()
185+
.values()
186+
.stream()
187+
.map(s -> "\"" + s + "\"")
188+
.collect(Collectors.joining(" || "));
189+
append(indentation, buffer, enumeration);
190+
break;
191+
case INT_ENUM:
192+
IntEnumShape intEnumShape = (IntEnumShape) target;
193+
String intEnumeration = intEnumShape.getEnumValues()
194+
.values()
195+
.stream()
196+
.map(i -> Integer.toString(i))
197+
.collect(Collectors.joining(" || "));
198+
append(indentation, buffer, intEnumeration);
199+
break;
200+
case OPERATION:
201+
case RESOURCE:
202+
case SERVICE:
203+
case MEMBER:
204+
default:
205+
append(indentation, buffer, "\"...\",");
206+
break;
207+
}
208+
}
209+
210+
switch (target.getType()) {
211+
case STRUCTURE:
212+
case UNION:
213+
case LIST:
214+
case SET:
215+
case MAP:
216+
break;
217+
case BIG_DECIMAL:
218+
case BIG_INTEGER:
219+
case BLOB:
220+
case BOOLEAN:
221+
case BYTE:
222+
case DOCUMENT:
223+
case DOUBLE:
224+
case ENUM:
225+
case FLOAT:
226+
case INTEGER:
227+
case INT_ENUM:
228+
case LONG:
229+
case MEMBER:
230+
case OPERATION:
231+
case RESOURCE:
232+
case SERVICE:
233+
case SHORT:
234+
case STRING:
235+
case TIMESTAMP:
236+
default:
237+
checkRequired(indentation, buffer, shape);
238+
break;
239+
}
240+
}
241+
242+
private static void checkRequired(int indentation, StringBuilder buffer, Shape shape) {
243+
if (shape.hasTrait(RequiredTrait.class)) {
244+
append(indentation, buffer, " // required\n");
245+
} else {
246+
append(indentation, buffer, "\n");
247+
}
248+
}
249+
250+
private static void append(int indentation, StringBuilder buffer, String tail) {
251+
while (indentation-- > 0) {
252+
buffer.append(" ");
253+
}
254+
buffer.append(tail);
255+
}
256+
257+
/**
258+
* Tracks the depths at which a shape appears in the tree.
259+
* If a shape appears at too many depths it is truncated.
260+
* This handles the case of recursive shapes.
261+
*/
262+
private static class ShapeTracker {
263+
private Map<Shape, Set<Integer>> data = new HashMap<Shape, Set<Integer>>();
264+
265+
/**
266+
* Mark that a shape is observed at depth.
267+
*/
268+
public void mark(Shape shape, int depth) {
269+
if (!data.containsKey(shape)) {
270+
data.put(shape, new HashSet<>());
271+
}
272+
data.get(shape).add(depth);
273+
}
274+
275+
/**
276+
* @return the number of distinct depths in which the shape appears.
277+
*/
278+
public int getOccurrenceDepths(Shape shape) {
279+
return data.getOrDefault(shape, Collections.emptySet()).size();
280+
}
281+
}
282+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package software.amazon.smithy.typescript.codegen.documentation;
2+
3+
import static org.hamcrest.MatcherAssert.assertThat;
4+
import static org.hamcrest.Matchers.equalTo;
5+
6+
import java.util.List;
7+
import org.junit.jupiter.api.Test;
8+
import software.amazon.smithy.model.Model;
9+
import software.amazon.smithy.model.shapes.ListShape;
10+
import software.amazon.smithy.model.shapes.MapShape;
11+
import software.amazon.smithy.model.shapes.MemberShape;
12+
import software.amazon.smithy.model.shapes.StringShape;
13+
import software.amazon.smithy.model.shapes.StructureShape;
14+
15+
public class StructureExampleGeneratorTest {
16+
17+
StringShape string = StringShape.builder()
18+
.id("foo.bar#string")
19+
.build();
20+
21+
ListShape list = ListShape.builder()
22+
.id("foo.bar#list")
23+
.member(string.getId())
24+
.build();
25+
26+
MapShape map = MapShape.builder()
27+
.id("foo.bar#map")
28+
.key(MemberShape.builder()
29+
.id("foo.bar#map$member")
30+
.target(string.getId())
31+
.build())
32+
.value(MemberShape.builder()
33+
.id("foo.bar#map$member")
34+
.target(string.getId())
35+
.build())
36+
.build();
37+
38+
MemberShape memberForString = MemberShape.builder()
39+
.id("foo.bar#structure$string")
40+
.target(string.getId())
41+
.build();
42+
43+
MemberShape memberForList = MemberShape.builder()
44+
.id("foo.bar#structure$list")
45+
.target(list.getId())
46+
.build();
47+
48+
MemberShape memberForMap = MemberShape.builder()
49+
.id("foo.bar#structure$map")
50+
.target(map.getId())
51+
.build();
52+
53+
StructureShape structure = StructureShape.builder()
54+
.id("foo.bar#structure")
55+
.members(
56+
List.<MemberShape>of(memberForString, memberForList, memberForMap))
57+
.build();
58+
59+
private Model model = Model.builder()
60+
.addShapes(
61+
string, list, map, structure,
62+
memberForString, memberForList, memberForMap)
63+
.build();
64+
65+
@Test
66+
public void generatesStructuralHintDocumentation_map() {
67+
assertThat(
68+
StructureExampleGenerator.generateStructuralHintDocumentation(map, model),
69+
equalTo("{\n \"<keys>\": \"STRING_VALUE\", \n},"));
70+
}
71+
72+
@Test
73+
public void generatesStructuralHintDocumentation_structure() {
74+
assertThat(
75+
StructureExampleGenerator.generateStructuralHintDocumentation(structure, model),
76+
equalTo("{\n"
77+
+ " string: \"STRING_VALUE\", \n"
78+
+ " list: [ \n"
79+
+ " \"STRING_VALUE\", \n"
80+
+ " ],\n"
81+
+ " map: { \n"
82+
+ " \"<keys>\": \"STRING_VALUE\", \n"
83+
+ " },\n"
84+
+ "}"));
85+
}
86+
87+
@Test
88+
public void generatesStructuralHintDocumentation_list() {
89+
assertThat(
90+
StructureExampleGenerator.generateStructuralHintDocumentation(list, model),
91+
equalTo("[\n \"STRING_VALUE\", \n],"));
92+
}
93+
}

0 commit comments

Comments
 (0)