Skip to content

Commit fed823a

Browse files
committed
open-api: add examples for request/response body
- format is: `{describe your json example here}`
1 parent 34bf57c commit fed823a

File tree

10 files changed

+131
-47
lines changed

10 files changed

+131
-47
lines changed

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/JavaDocSetter.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static void set(OperationExt operation, MethodDoc doc, List<String> param
4848
if (!doc.getExtensions().isEmpty()) {
4949
operation.setExtensions(doc.getExtensions());
5050
}
51-
doc.getSecurityRequeriments().forEach(operation::addSecurityItem);
51+
doc.getSecurityRequirements().forEach(operation::addSecurityItem);
5252
doc.getTags().forEach(operation::addTag);
5353
// Parameters
5454
for (var parameterName : parameterNames) {
@@ -63,16 +63,19 @@ public static void set(OperationExt operation, MethodDoc doc, List<String> param
6363
if (paramExt == null) {
6464
var body = operation.getRequestBody();
6565
if (body != null) {
66+
body.setExamples(doc.getParameterExample(parameterName));
6667
body.setDescription(paramDoc);
6768
}
6869
} else {
70+
paramExt.setExample(doc.getParameterExample(parameterName));
6971
paramExt.setDescription(paramDoc);
7072
}
7173
}
7274
}
7375
// return types
7476
var defaultResponse = operation.getDefaultResponse();
7577
if (defaultResponse != null) {
78+
defaultResponse.setExamples(doc.getReturnExample());
7679
defaultResponse.setDescription(doc.getReturnDoc());
7780
}
7881
for (var throwsDoc : doc.getThrows().values()) {

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RequestBodyExt.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
public class RequestBodyExt extends RequestBody {
1313

14+
@JsonIgnore private Object examples;
1415
@JsonIgnore private String javaType;
1516

1617
@JsonIgnore private String contentType = MediaType.JSON;
@@ -34,4 +35,12 @@ public String getContentType() {
3435
public void setContentType(String contentType) {
3536
this.contentType = contentType;
3637
}
38+
39+
public Object getExamples() {
40+
return examples;
41+
}
42+
43+
public void setExamples(Object examples) {
44+
this.examples = examples;
45+
}
3746
}

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/ResponseExt.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class ResponseExt extends ApiResponse {
2929
"io.smallrye.mutiny.Uni",
3030
"io.smallrye.mutiny.Multi");
3131

32+
@JsonIgnore private Object examples;
3233
@JsonIgnore private List<String> javaTypes = new ArrayList<>();
3334

3435
@JsonIgnore private String code;
@@ -84,6 +85,14 @@ public void setCode(String code) {
8485
this.code = code;
8586
}
8687

88+
public Object getExamples() {
89+
return examples;
90+
}
91+
92+
public void setExamples(Object examples) {
93+
this.examples = examples;
94+
}
95+
8796
@Override
8897
public String toString() {
8998
return getJavaType();

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,14 @@ private void checkResponse(
144144
}
145145

146146
if (content.isEmpty()) {
147-
io.swagger.v3.oas.models.media.MediaType mediaTypeObject =
148-
new io.swagger.v3.oas.models.media.MediaType();
147+
var mediaTypeObject = new io.swagger.v3.oas.models.media.MediaType();
149148
String mediaType = operation.getProduces().stream().findFirst().orElse(MediaType.JSON);
150149
content.addMediaType(mediaType, mediaTypeObject);
151150
}
152151
if (isSuccessCode(statusCode)) {
153-
for (io.swagger.v3.oas.models.media.MediaType mediaType : content.values()) {
154-
Schema schema = mediaType.getSchema();
152+
for (var mediaType : content.values()) {
153+
Optional.ofNullable(response.getExamples()).ifPresent(mediaType::setExample);
154+
var schema = mediaType.getSchema();
155155
if (schema == null) {
156156
mediaType.setSchema(defaultSchema);
157157
}
@@ -174,6 +174,11 @@ private void checkRequestBody(ParserContext ctx, OperationExt operation) {
174174
content.addMediaType(mediaTypeName, mediaType);
175175
requestBody.setContent(content);
176176
}
177+
if (requestBody.getExamples() != null) {
178+
requestBody
179+
.getContent()
180+
.forEach((key, value) -> value.setExample(requestBody.getExamples()));
181+
}
177182
}
178183
}
179184

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocObjectParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,14 @@ private static Map<String, Object> buildMapFromPairs(List<String> pairs) {
298298
// Case 3: Simple key-value pairs, possibly nested (e.g., "address.country").
299299
for (String path : pathsInGroup) {
300300
String value = pathValues.get(path).get(0);
301-
setValue(resultMap, path, parseValue(value));
301+
setValue(resultMap, path, parseJson(value));
302302
}
303303
}
304304
}
305305
return resultMap;
306306
}
307307

308-
private static Object parseValue(String value) {
308+
public static Object parseJson(String value) {
309309
try {
310310
return new UnquotedJsonParser().parse(value);
311311
} catch (Exception ignored) {

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocTag.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,46 @@ public class JavaDocTag {
5454
CUSTOM_TAG.and(it -> it.getText().startsWith("@x-"));
5555
private static final Predicate<DetailNode> THROWS =
5656
it -> tree(it).anyMatch(javadocToken(JavadocTokenTypes.THROWS_LITERAL));
57+
private static final Predicate<DetailNode> PARAM =
58+
it -> tree(it).anyMatch(javadocToken(JavadocTokenTypes.PARAM_LITERAL));
59+
60+
public static Map<String, String> getParametersDoc(DetailNode node) {
61+
var parameters = new LinkedHashMap<String, String>();
62+
javaDocTag(
63+
node,
64+
PARAM,
65+
(tag, value) -> {
66+
children(tag)
67+
.filter(javadocToken(JavadocTokenTypes.PARAMETER_NAME))
68+
.findFirst()
69+
.map(DetailNode::getText)
70+
.ifPresent(
71+
name -> {
72+
children(tag)
73+
.filter(javadocToken(JavadocTokenTypes.DESCRIPTION))
74+
.findFirst()
75+
.map(description -> JavaDocNode.getText(tree(description).toList(), true))
76+
.ifPresent(text -> parameters.put(name, text));
77+
});
78+
});
79+
return parameters;
80+
}
81+
82+
public static String getReturnDoc(DetailNode node) {
83+
var text = new StringBuilder();
84+
javaDocTag(
85+
node,
86+
javadocToken(JavadocTokenTypes.RETURN_LITERAL),
87+
(tag, value) -> {
88+
children(tag.getParent())
89+
.filter(javadocToken(JavadocTokenTypes.DESCRIPTION))
90+
.findFirst()
91+
.map(description -> JavaDocNode.getText(tree(description).toList(), true))
92+
.ifPresent(text::append);
93+
});
94+
var result = text.toString().trim();
95+
return result.isEmpty() ? null : result;
96+
}
5797

5898
public static List<SecurityRequirement> securityRequirement(DetailNode node) {
5999
return parse(node, SECURITY, null).stream()

modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/MethodDoc.java

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import static io.jooby.internal.openapi.javadoc.JavaDocStream.*;
99

1010
import java.util.*;
11-
import java.util.stream.Stream;
1211

1312
import com.puppycrawl.tools.checkstyle.api.DetailAST;
1413
import com.puppycrawl.tools.checkstyle.api.DetailNode;
@@ -19,16 +18,20 @@
1918
import io.swagger.v3.oas.models.security.SecurityRequirement;
2019

2120
public class MethodDoc extends JavaDocNode {
21+
private Map<String, String> parameters;
2222
private List<SecurityRequirement> securityRequeriments;
2323
private String operationId;
2424
private Map<StatusCode, ResponseExt> throwList;
2525
private List<String> parameterTypes = null;
26+
private String returnDoc;
2627

2728
public MethodDoc(JavaDocParser ctx, DetailAST node, DetailAST javadoc) {
2829
super(ctx, node, javadoc);
2930
throwList = JavaDocTag.throwList(this.javadoc);
3031
operationId = JavaDocTag.operationId(this.javadoc);
3132
securityRequeriments = JavaDocTag.securityRequirement(this.javadoc);
33+
parameters = JavaDocTag.getParametersDoc(this.javadoc);
34+
returnDoc = JavaDocTag.getReturnDoc(this.javadoc);
3235
}
3336

3437
MethodDoc(JavaDocParser ctx, DetailAST node, DetailNode javadoc) {
@@ -47,7 +50,7 @@ public void setOperationId(String operationId) {
4750
this.operationId = operationId;
4851
}
4952

50-
public List<SecurityRequirement> getSecurityRequeriments() {
53+
public List<SecurityRequirement> getSecurityRequirements() {
5154
return securityRequeriments;
5255
}
5356

@@ -94,43 +97,52 @@ public List<String> getJavadocParameterNames() {
9497
}
9598

9699
public String getParameterDoc(String name) {
97-
return tree(javadoc)
98-
// must be a tag
99-
.filter(javadocToken(JavadocTokenTypes.JAVADOC_TAG))
100-
.filter(
101-
it -> {
102-
var children = children(it).toList();
103-
return children.stream()
104-
.anyMatch(
105-
t ->
106-
t.getType() == JavadocTokenTypes.PARAM_LITERAL
107-
&& t.getText().equals("@param"))
108-
&& children.stream().anyMatch(t -> t.getText().equals(name));
109-
})
110-
.findFirst()
111-
.map(
112-
it ->
113-
getText(
114-
Stream.of(it.getChildren())
115-
.filter(e -> e.getType() == JavadocTokenTypes.DESCRIPTION)
116-
.flatMap(JavaDocStream::tree)
117-
.toList(),
118-
true))
119-
.filter(it -> !it.isEmpty())
120-
.orElse(null);
100+
var doc = parameters.get(name);
101+
return doc == null ? null : doc.replace(exampleCode(doc), "").trim();
102+
}
103+
104+
public Object getParameterExample(String name) {
105+
return toExamples(exampleCode(parameters.get(name)));
106+
}
107+
108+
private String exampleCode(String text) {
109+
if (text == null) {
110+
return "";
111+
}
112+
var start = text.indexOf("`");
113+
if (start == -1) {
114+
return "";
115+
}
116+
var end = text.indexOf("`", start + 1);
117+
if (end == -1) {
118+
return "";
119+
}
120+
return text.substring(start, end + 1);
121+
}
122+
123+
private Object toExamples(String text) {
124+
var codeExample = exampleCode(text);
125+
if (codeExample.isEmpty()) {
126+
return null;
127+
}
128+
var clean = codeExample.substring(1, codeExample.length() - 1);
129+
var result = JavaDocObjectParser.parseJson(clean);
130+
if (result.equals(codeExample)) {
131+
// Like a primitive/basic example
132+
return List.of(result);
133+
}
134+
return result;
121135
}
122136

123137
public String getReturnDoc() {
124-
return tree(javadoc)
125-
.filter(javadocToken(JavadocTokenTypes.RETURN_LITERAL))
126-
.findFirst()
127-
.flatMap(
128-
it ->
129-
tree(it.getParent())
130-
.filter(javadocToken(JavadocTokenTypes.DESCRIPTION))
131-
.findFirst())
132-
.map(it -> getText(tree(it).toList(), true))
133-
.orElse(null);
138+
if (returnDoc != null) {
139+
return returnDoc.replace(exampleCode(returnDoc), "").trim();
140+
}
141+
return null;
142+
}
143+
144+
public Object getReturnExample() {
145+
return toExamples(returnDoc);
134146
}
135147

136148
public Map<StatusCode, ResponseExt> getThrows() {

modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ private void checkResult(OpenAPIResult result) {
151151
+ " application/json:\n"
152152
+ " schema:\n"
153153
+ " $ref: \"#/components/schemas/Book\"\n"
154+
+ " example:\n"
155+
+ " isbn: X01981\n"
156+
+ " title: HarryPotter\n"
154157
+ " required: true\n"
155158
+ " responses:\n"
156159
+ " \"200\":\n"
@@ -159,6 +162,9 @@ private void checkResult(OpenAPIResult result) {
159162
+ " application/json:\n"
160163
+ " schema:\n"
161164
+ " $ref: \"#/components/schemas/Book\"\n"
165+
+ " example:\n"
166+
+ " id: generatedId\n"
167+
+ " isbn: '...'\n"
162168
+ "components:\n"
163169
+ " schemas:\n"
164170
+ " Author:\n"

modules/jooby-openapi/src/test/java/issues/i3729/api/LibraryApi.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ public List<Book> query(@QueryParam BookQuery query) {
6666
*
6767
* <p>Book can be created or updated.
6868
*
69-
* @param book Book to create.
70-
* @return Saved book.
69+
* @param book Book to create. `{isbn: X01981, title: HarryPotter}`
70+
* @return Saved book. `{id: generatedId, isbn: ...}`
7171
* @tag Author
7272
*/
7373
@POST

modules/jooby-openapi/src/test/java/issues/i3729/api/ScriptLibrary.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ public class ScriptLibrary extends Jooby {
7878
*
7979
* <p>Book can be created or updated.
8080
*
81-
* @param book Book to create.
82-
* @return Saved book.
81+
* @param book Book to create. `{isbn: X01981, title: HarryPotter}`
82+
* @return Saved book. `{id: generatedId, isbn: ...}`
8383
* @tag Author
8484
* @operationId createBook
8585
*/

0 commit comments

Comments
 (0)