Skip to content

Commit d63792f

Browse files
committed
openapi: more clean up + support return doc + throws
- ref #3733
1 parent 8554fda commit d63792f

File tree

10 files changed

+213
-23
lines changed

10 files changed

+213
-23
lines changed

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ public static List<OperationExt> parse(ParserContext ctx, String prefix, Type ty
290290
methodDoc -> {
291291
operationExt.setSummary(methodDoc.getSummary());
292292
operationExt.setDescription(methodDoc.getDescription());
293+
// Parameters
293294
for (var parameterName : parameterNames) {
294295
var paramExt =
295296
operationExt.getParameters().stream()
@@ -311,14 +312,21 @@ public static List<OperationExt> parse(ParserContext ctx, String prefix, Type ty
311312
}
312313
}
313314
}
314-
for (var parameter : operationExt.getParameters()) {
315-
var paramExt = (ParameterExt) parameter;
316-
var paramDoc =
317-
methodDoc.getParameterDoc(
318-
paramExt.getName(), paramExt.getContainerType());
319-
if (paramDoc != null) {
320-
paramExt.setDescription(paramDoc);
315+
// return types
316+
var defaultResponse = operationExt.getDefaultResponse();
317+
if (defaultResponse != null) {
318+
defaultResponse.setDescription(methodDoc.getReturnDoc());
319+
}
320+
for (var throwsDoc : methodDoc.getThrows().values()) {
321+
var response =
322+
operationExt.getResponse(
323+
Integer.toString(throwsDoc.getStatusCode().value()));
324+
if (response == null) {
325+
response =
326+
operationExt.addResponse(
327+
Integer.toString(throwsDoc.getStatusCode().value()));
321328
}
329+
response.setDescription(throwsDoc.getText());
322330
}
323331
});
324332
});
@@ -445,7 +453,7 @@ private static List<ParameterExt> routerArguments(
445453

446454
if (paramType == ParamType.BODY) {
447455
RequestBodyExt body = new RequestBodyExt();
448-
body.setRequired(required);
456+
body.setRequired(true);
449457
body.setJavaType(javaType);
450458
requestBody.accept(body);
451459
} else if (paramType == ParamType.FORM) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.swagger.v3.oas.models.parameters.RequestBody;
1111

1212
public class RequestBodyExt extends RequestBody {
13+
1314
@JsonIgnore private String javaType;
1415

1516
@JsonIgnore private String contentType = MediaType.JSON;

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import io.jooby.Router;
4848
import io.jooby.SneakyThrows;
4949
import io.jooby.annotation.OpenApiRegister;
50+
import io.swagger.v3.core.util.Json;
5051
import io.swagger.v3.oas.models.media.ComposedSchema;
5152
import io.swagger.v3.oas.models.media.Content;
5253
import io.swagger.v3.oas.models.media.Schema;
@@ -171,11 +172,15 @@ private List<Parameter> checkParameters(ParserContext ctx, List<Parameter> param
171172
for (Object e : ref.schema.getProperties().entrySet()) {
172173
String name = (String) ((Map.Entry) e).getKey();
173174
Schema s = (Schema) ((Map.Entry) e).getValue();
175+
var schemaMap = Json.mapper().convertValue(s, Map.class);
176+
schemaMap.remove("description");
177+
var schemaNoDesc = Json.mapper().convertValue(schemaMap, Schema.class);
174178
ParameterExt p = new ParameterExt();
175179
p.setContainerType(javaType);
176180
p.setName(name);
177181
p.setIn(parameter.getIn());
178-
p.setSchema(s);
182+
p.setSchema(schemaNoDesc);
183+
p.setDescription(parameter.getDescription());
179184

180185
params.add(p);
181186
}

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

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,81 @@
77

88
import static io.jooby.internal.openapi.javadoc.JavaDocSupport.*;
99

10-
import java.util.ArrayList;
11-
import java.util.List;
10+
import java.util.*;
1211
import java.util.stream.Stream;
1312

1413
import com.puppycrawl.tools.checkstyle.api.DetailAST;
1514
import com.puppycrawl.tools.checkstyle.api.DetailNode;
1615
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
1716
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
17+
import io.jooby.StatusCode;
1818

1919
public class MethodDoc extends JavaDocNode {
20+
21+
private Map<StatusCode, ThrowsDoc> throwList;
22+
2023
public MethodDoc(JavaDocParser ctx, DetailAST node, DetailAST javadoc) {
2124
super(ctx, node, javadoc);
25+
throwList = throwList(this.javadoc);
26+
}
27+
28+
private Map<StatusCode, ThrowsDoc> throwList(DetailNode javadoc) {
29+
var result = new LinkedHashMap<StatusCode, ThrowsDoc>();
30+
for (var tag : tree(javadoc).filter(javadocToken(JavadocTokenTypes.JAVADOC_TAG)).toList()) {
31+
var isThrows = tree(tag).anyMatch(javadocToken(JavadocTokenTypes.THROWS_LITERAL));
32+
if (isThrows) {
33+
var text =
34+
tree(tag)
35+
.filter(javadocToken(JavadocTokenTypes.DESCRIPTION))
36+
.findFirst()
37+
.map(it -> getText(List.of(it.getChildren()), true))
38+
.orElse(null);
39+
var statusCode =
40+
tree(tag)
41+
.filter(javadocToken(JavadocTokenTypes.DESCRIPTION))
42+
.findFirst()
43+
.flatMap(
44+
it ->
45+
tree(it)
46+
.filter(javadocToken(JavadocTokenTypes.HTML_TAG_NAME))
47+
.filter(tagName -> tagName.getText().equals("code"))
48+
.flatMap(
49+
tagName ->
50+
backward(tagName)
51+
.filter(javadocToken(JavadocTokenTypes.HTML_TAG))
52+
.findFirst()
53+
.stream())
54+
.flatMap(
55+
htmlTag ->
56+
children(htmlTag)
57+
.filter(javadocToken(JavadocTokenTypes.TEXT))
58+
.findFirst()
59+
.stream())
60+
.map(DetailNode::getText)
61+
.map(
62+
value -> {
63+
try {
64+
return Integer.parseInt(value);
65+
} catch (NumberFormatException e) {
66+
return null;
67+
}
68+
})
69+
.filter(Objects::nonNull)
70+
.filter(code -> code >= 400 && code <= 600)
71+
.map(StatusCode::valueOf)
72+
.findFirst())
73+
.orElse(null);
74+
// var className = tree(tag).filter(javadocToken(JavadocTokenTypes.CLASS_NAME))
75+
// .findFirst()
76+
// .map(DetailNode::getText)
77+
// .orElse(null);
78+
if (statusCode != null) {
79+
var throwsDoc = new ThrowsDoc(statusCode, text);
80+
result.putIfAbsent(statusCode, throwsDoc);
81+
}
82+
}
83+
}
84+
return result;
2285
}
2386

2487
MethodDoc(JavaDocParser ctx, DetailAST node, DetailNode javadoc) {
@@ -91,4 +154,8 @@ public String getReturnDoc() {
91154
.map(it -> getText(tree(it).toList(), true))
92155
.orElse(null);
93156
}
157+
158+
public Map<StatusCode, ThrowsDoc> getThrows() {
159+
return throwList;
160+
}
94161
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.openapi.javadoc;
7+
8+
import io.jooby.StatusCode;
9+
10+
public class ThrowsDoc {
11+
private final String text;
12+
private final StatusCode statusCode;
13+
14+
public ThrowsDoc(StatusCode statusCode, String text) {
15+
this.statusCode = statusCode;
16+
if (text == null) {
17+
this.text = statusCode.reason();
18+
} else {
19+
this.text = statusCode.reason() + ": " + text;
20+
}
21+
}
22+
23+
public String getText() {
24+
return text;
25+
}
26+
27+
public StatusCode getStatusCode() {
28+
return statusCode;
29+
}
30+
}

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

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,34 +37,80 @@ public void shouldGenerateDoc(OpenAPIResult result) {
3737
+ " type: string\n"
3838
+ " responses:\n"
3939
+ " \"200\":\n"
40-
+ " description: Success\n"
40+
+ " description: A matching book.\n"
4141
+ " content:\n"
4242
+ " application/json:\n"
4343
+ " schema:\n"
4444
+ " $ref: \"#/components/schemas/Book\"\n"
45+
+ " \"404\":\n"
46+
+ " description: \"Not Found: If a book doesn't exist.\"\n"
47+
+ " \"400\":\n"
48+
+ " description: \"Bad Request: For bad ISBN code.\"\n"
4549
+ " /api/library:\n"
4650
+ " summary: Library API.\n"
4751
+ " description: \"Contains all operations for creating, updating and fetching"
4852
+ " books.\"\n"
53+
+ " get:\n"
54+
+ " summary: Query books.\n"
55+
+ " operationId: query\n"
56+
+ " parameters:\n"
57+
+ " - name: title\n"
58+
+ " in: query\n"
59+
+ " description: Book's param query.\n"
60+
+ " schema:\n"
61+
+ " type: string\n"
62+
+ " - name: author\n"
63+
+ " in: query\n"
64+
+ " description: Book's param query.\n"
65+
+ " schema:\n"
66+
+ " type: string\n"
67+
+ " - name: isbn\n"
68+
+ " in: query\n"
69+
+ " description: Book's param query.\n"
70+
+ " schema:\n"
71+
+ " type: string\n"
72+
+ " responses:\n"
73+
+ " \"200\":\n"
74+
+ " description: Matching books.\n"
75+
+ " content:\n"
76+
+ " application/json:\n"
77+
+ " schema:\n"
78+
+ " type: array\n"
79+
+ " items:\n"
80+
+ " $ref: \"#/components/schemas/Book\"\n"
4981
+ " post:\n"
5082
+ " summary: Creates a new book.\n"
83+
+ " description: Book can be created or updated.\n"
5184
+ " operationId: createBook\n"
5285
+ " requestBody:\n"
5386
+ " description: Book to create.\n"
5487
+ " content:\n"
5588
+ " application/json:\n"
5689
+ " schema:\n"
5790
+ " $ref: \"#/components/schemas/Book\"\n"
58-
+ " required: false\n"
91+
+ " required: true\n"
5992
+ " responses:\n"
6093
+ " \"200\":\n"
61-
+ " description: Success\n"
94+
+ " description: Saved book.\n"
6295
+ " content:\n"
6396
+ " application/json:\n"
6497
+ " schema:\n"
6598
+ " $ref: \"#/components/schemas/Book\"\n"
6699
+ "components:\n"
67100
+ " schemas:\n"
101+
+ " BookQuery:\n"
102+
+ " type: object\n"
103+
+ " properties:\n"
104+
+ " title:\n"
105+
+ " type: string\n"
106+
+ " description: Book's title. Optional.\n"
107+
+ " author:\n"
108+
+ " type: string\n"
109+
+ " description: Book's author. Optional.\n"
110+
+ " isbn:\n"
111+
+ " type: string\n"
112+
+ " description: Book's isbn. Optional.\n"
113+
+ " description: Query books by complex filters.\n"
68114
+ " Address:\n"
69115
+ " type: object\n"
70116
+ " properties:\n"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package issues.i3729.api;
7+
8+
/**
9+
* Query books by complex filters.
10+
*
11+
* @param title Book's title. Optional.
12+
* @param author Book's author. Optional.
13+
* @param isbn Book's isbn. Optional.
14+
*/
15+
public record BookQuery(String title, String author, String isbn) {}

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
*/
66
package issues.i3729.api;
77

8-
import io.jooby.annotation.GET;
9-
import io.jooby.annotation.POST;
10-
import io.jooby.annotation.Path;
11-
import io.jooby.annotation.PathParam;
8+
import java.util.List;
9+
10+
import io.jooby.annotation.*;
11+
import io.jooby.exception.BadRequestException;
12+
import io.jooby.exception.NotFoundException;
1213

1314
/**
1415
* Library API.
@@ -22,16 +23,31 @@ public class LibraryApi {
2223
* Find a book by isbn.
2324
*
2425
* @param isbn Book isbn. Like IK-1900.
25-
* @return A book
26+
* @return A matching book.
27+
* @throws NotFoundException <code>404</code> If a book doesn't exist.
28+
* @throws BadRequestException <code>400</code> For bad ISBN code.
2629
*/
2730
@GET("/{isbn}")
28-
public Book bookByIsbn(@PathParam String isbn) {
31+
public Book bookByIsbn(@PathParam String isbn) throws NotFoundException, BadRequestException {
2932
return new Book();
3033
}
3134

35+
/**
36+
* Query books.
37+
*
38+
* @param query Book's param query.
39+
* @return Matching books.
40+
*/
41+
@GET
42+
public List<Book> query(@QueryParam BookQuery query) {
43+
return List.of(new Book());
44+
}
45+
3246
/**
3347
* Creates a new book.
3448
*
49+
* <p>Book can be created or updated.
50+
*
3551
* @param book Book to create.
3652
* @return Saved book.
3753
*/

modules/jooby-openapi/src/test/java/javadoc/JavaDocParserTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ public void shouldParseEnum() throws Exception {
133133
assertEquals("Enum summary.", doc.getSummary());
134134
assertEquals("Enum desc.", doc.getDescription());
135135
assertEquals(
136-
"Enum summary.\n" + " - Foo: Foo doc.\n" + " - Bar: Bar doc.", doc.getText());
136+
"Enum summary.\n" + " - Foo: Foo doc.\n" + " - Bar: Bar doc.",
137+
doc.getEnumDescription(doc.getSummary()));
137138
});
138139
}
139140

modules/jooby-openapi/src/test/java/javadoc/PrintAstTree.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import java.io.IOException;
1010
import java.nio.file.Path;
1111
import java.nio.file.Paths;
12-
import javadoc.input.NoDoc;
1312

1413
import com.puppycrawl.tools.checkstyle.AstTreeStringPrinter;
1514
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
15+
import issues.i3729.api.LibraryApi;
1616

1717
public class PrintAstTree {
1818
public static void main(String[] args) throws CheckstyleException, IOException {
@@ -24,7 +24,8 @@ public static void main(String[] args) throws CheckstyleException, IOException {
2424
.resolve("test")
2525
.resolve("java");
2626
var stringAst =
27-
AstTreeStringPrinter.printJavaAndJavadocTree(baseDir.resolve(toPath(NoDoc.class)).toFile());
27+
AstTreeStringPrinter.printJavaAndJavadocTree(
28+
baseDir.resolve(toPath(LibraryApi.class)).toFile());
2829
System.out.println(stringAst);
2930
}
3031

0 commit comments

Comments
 (0)