Skip to content

Commit f9104d3

Browse files
committed
open-api: document usage
- add few more tags - #3729
1 parent 9bbbad4 commit f9104d3

File tree

6 files changed

+271
-16
lines changed

6 files changed

+271
-16
lines changed

docs/asciidoc/modules/openapi.adoc

Lines changed: 187 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This module helps automating the generation of API documentation using Jooby projects. jooby-openapi works by examining an application at *build* time to infer API semantics based on byte code and optional annotations.
44

5-
Automatically generates documentation in JSON/YAML format APIs. This documentation can be completed by comments using swagger-api annotations.
5+
Automatically generates documentation in JSON/YAML format APIs. This documentation can be completed by javadoc comments or using swagger-api annotations.
66

77
This library supports:
88

@@ -148,12 +148,20 @@ The OpenAPI generator works exactly the same for MVC routes (a.k.a Controller):
148148
{
149149
install(new OpenAPIModule());
150150
151-
mvc(new Pets());
151+
mvc(new Pets_());
152152
}
153153
154+
/**
155+
* Pet Library.
156+
*/
154157
@Path("/pets")
155158
public class Pets {
156159
160+
/**
161+
* List pets.
162+
*
163+
* @return All pets.
164+
*/
157165
@GET
158166
public List<Pet> list() {
159167
...
@@ -168,7 +176,7 @@ public class Pets {
168176
{
169177
install(OpenAPIModule())
170178
171-
mvc(new MyController())
179+
mvc(Pets_())
172180
}
173181
174182
@Path("/pets")
@@ -185,9 +193,184 @@ class Pets {
185193
The Maven plugin and Gradle task provide two filter properties `includes` and `excludes`. These
186194
properties filter routes by their path pattern. The filter is a regular expression.
187195

196+
=== JavaDoc comments
197+
198+
Parsing of JavaDoc comment is supported on Java MVC/Controller.
199+
200+
.Java
201+
[source,java]
202+
----
203+
204+
/**
205+
* My Library.
206+
* @version 5.4.1
207+
* @tag Books. All about books.
208+
* @server.url https://books.api.com
209+
* @server.description Production
210+
* @x-logo https://my.api.com/logo.png
211+
*/
212+
public class App {
213+
{
214+
mvc(new Books_());
215+
install(new OpenAPIModule());
216+
}
217+
}
218+
/**
219+
* Books operations.
220+
* @tag Books
221+
*/
222+
@Path("/api/books")
223+
public class Books {
224+
/**
225+
* Query and filter books.
226+
*
227+
* @param query Book filter.
228+
* @return List of matching books.
229+
*/
230+
public List<Book> query(@QueryParam BookQuery query) {
231+
....
232+
}
233+
/**
234+
* Find a book by ISBN.
235+
*
236+
* @param isbn ISBN code.
237+
* @return A book.
238+
* @throws BookNotFoundException When a book doesn't exist. <code>404</code>
239+
*/
240+
public Book bookByIsbn(@PathParam String isbn) {
241+
242+
}
243+
}
244+
245+
/**
246+
* Book query.
247+
*
248+
* @type Book type.
249+
* @aga Book age.
250+
*/
251+
public record BookQuery(BookType type, int age) {}
252+
253+
/**
254+
* Books can be broadly categorized into fiction and non-fiction
255+
*/
256+
public enum BookType {
257+
/**
258+
* Fiction includes genres like fantasy, science fiction, romance, and mystery.
259+
*/
260+
Fiction,
261+
/**
262+
* Non-fiction encompasses genres like history, biography, and self-help.
263+
*/
264+
NonFiction
265+
}
266+
----
267+
268+
A JavaDoc comment is split into:
269+
270+
- summary: Everything before the first `.` or `<p>` paragraph
271+
- description: Everything after the first `.` or `<p>` paragraph
272+
273+
Whitespaces (including new lines) are ignored. To introduce a new line, you must use a `<p>`.
274+
275+
==== Supported OpenAPI tags
276+
277+
[cols="3,1,1,1,4"]
278+
|===
279+
| Tag | Main | Controller | Method | Description
280+
281+
|@version 4.0.4
282+
| [x]
283+
|
284+
|
285+
|
286+
287+
|@contact.name
288+
|[x]
289+
|
290+
|
291+
|
292+
293+
|@contact.url
294+
|[x]
295+
|
296+
|
297+
|
298+
299+
|@contact.email
300+
|[x]
301+
|
302+
|
303+
|
304+
305+
|@license.name
306+
|[x]
307+
|
308+
|
309+
|
310+
311+
|@license.url
312+
|[x]
313+
|
314+
|
315+
|
316+
317+
|@server.description
318+
|[x]
319+
|
320+
|
321+
|
322+
323+
|@x-
324+
|[x]
325+
|[x]
326+
|[x]
327+
|Tag starting with `x-` is considered an extension
328+
329+
|@tag.name
330+
|[x]
331+
|[x]
332+
|[x]
333+
|Tag name
334+
335+
|@tag.description
336+
|[x]
337+
|[x]
338+
|[x]
339+
|Tag Description
340+
341+
|@tag
342+
|[x]
343+
|[x]
344+
|[x]
345+
|Shortcut for previous `@tag.name` and `@tag.description`. The tag name is everything before `.`
346+
347+
|@param <name>
348+
|
349+
|
350+
|[x]
351+
|Operation parameter
352+
353+
|@return
354+
|
355+
|
356+
|[x]
357+
|Operation default response
358+
359+
|@throws
360+
|
361+
|
362+
|[x]
363+
|Additional non-success responses types. The HTTP response code is taken from `<code>{number}</code>`, or set to `500` error.
364+
365+
|===
366+
367+
This feature is only available for Java MVC/controller routes. There are plans to support lambda
368+
routes on Java. Kotlin source code is not supported.
369+
370+
188371
=== Annotations
189372

190-
To produces a better documentation this plugin depends on some OpenAPI annotations. To use them, you
373+
To produce a better documentation, this plugin depends on some OpenAPI annotations. To use them, you
191374
need to add a dependency to your project:
192375

193376
[dependency, artifactId="swagger-annotations"]

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@
1717
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
1818
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
1919
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
20+
import io.swagger.v3.oas.models.info.Contact;
21+
import io.swagger.v3.oas.models.info.License;
2022
import io.swagger.v3.oas.models.servers.Server;
2123

2224
public class ClassDoc extends JavaDocNode {
2325
private final Map<String, FieldDoc> fields = new LinkedHashMap<>();
2426
private final Map<String, MethodDoc> methods = new LinkedHashMap<>();
2527
private final List<Server> servers;
28+
private final List<Contact> contact;
29+
private final List<License> license;
2630

2731
public ClassDoc(JavaDocParser ctx, DetailAST node, DetailAST javaDoc) {
2832
super(ctx, node, javaDoc);
@@ -32,12 +36,22 @@ public ClassDoc(JavaDocParser ctx, DetailAST node, DetailAST javaDoc) {
3236
defaultEnumMembers();
3337
}
3438
this.servers = JavaDocTag.servers(this.javadoc);
39+
this.contact = JavaDocTag.contacts(this.javadoc);
40+
this.license = JavaDocTag.license(this.javadoc);
3541
}
3642

3743
public List<Server> getServers() {
3844
return servers;
3945
}
4046

47+
public List<Contact> getContact() {
48+
return contact;
49+
}
50+
51+
public List<License> getLicense() {
52+
return license;
53+
}
54+
4155
public String getVersion() {
4256
return tree(javadoc)
4357
.filter(javadocToken(JavadocTokenTypes.VERSION_LITERAL))

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

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
import static io.jooby.internal.openapi.javadoc.JavaDocSupport.children;
1111

1212
import java.util.*;
13+
import java.util.function.Function;
1314
import java.util.function.Predicate;
1415

1516
import com.puppycrawl.tools.checkstyle.api.DetailNode;
1617
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
1718
import io.jooby.SneakyThrows.Consumer2;
1819
import io.jooby.SneakyThrows.Consumer3;
1920
import io.jooby.StatusCode;
21+
import io.swagger.v3.oas.models.info.Contact;
22+
import io.swagger.v3.oas.models.info.License;
2023
import io.swagger.v3.oas.models.servers.Server;
2124
import io.swagger.v3.oas.models.tags.Tag;
2225

@@ -27,36 +30,77 @@ public class JavaDocTag {
2730
CUSTOM_TAG.and(it -> it.getText().startsWith("@tag.") || it.getText().equals("@tag"));
2831
private static final Predicate<DetailNode> SERVER =
2932
CUSTOM_TAG.and(it -> it.getText().startsWith("@server."));
33+
private static final Predicate<DetailNode> CONTACT =
34+
CUSTOM_TAG.and(it -> it.getText().startsWith("@contact."));
35+
private static final Predicate<DetailNode> LICENSE =
36+
CUSTOM_TAG.and(it -> it.getText().startsWith("@license."));
3037
private static final Predicate<DetailNode> EXTENSION =
3138
CUSTOM_TAG.and(it -> it.getText().startsWith("@x-"));
3239
private static final Predicate<DetailNode> THROWS =
3340
it -> tree(it).anyMatch(javadocToken(JavadocTokenTypes.THROWS_LITERAL));
3441

35-
@SuppressWarnings("unchecked")
3642
public static List<Server> servers(DetailNode node) {
43+
return openApiComponent(
44+
node,
45+
SERVER,
46+
"server",
47+
hash -> {
48+
var server = new Server();
49+
server.setDescription((String) hash.get("description"));
50+
server.setUrl((String) hash.get("url"));
51+
return server;
52+
});
53+
}
54+
55+
public static List<Contact> contacts(DetailNode node) {
56+
return openApiComponent(
57+
node,
58+
CONTACT,
59+
"contact",
60+
hash -> {
61+
var item = new Contact();
62+
item.setName((String) hash.get("name"));
63+
item.setUrl((String) hash.get("url"));
64+
item.setEmail((String) hash.get("email"));
65+
return item;
66+
});
67+
}
68+
69+
public static List<License> license(DetailNode node) {
70+
return openApiComponent(
71+
node,
72+
LICENSE,
73+
"license",
74+
hash -> {
75+
var item = new License();
76+
item.setName((String) hash.get("name"));
77+
item.setUrl((String) hash.get("url"));
78+
return item;
79+
});
80+
}
81+
82+
private static <T> List<T> openApiComponent(
83+
DetailNode node, Predicate<DetailNode> filter, String path, Function<Map<?, ?>, T> mapper) {
3784
var values = new ArrayList<String>();
3885
javaDocTag(
3986
node,
40-
SERVER,
87+
filter,
4188
(tag, value) -> {
4289
values.add(tag.getText().substring(1));
4390
values.add(value);
4491
});
45-
var result = new ArrayList<Server>();
92+
var result = new ArrayList<T>();
4693
if (!values.isEmpty()) {
47-
var serverMap = MiniYamlDocParser.parse(values);
48-
var servers = serverMap.get("server");
49-
if (!(servers instanceof List<?>)) {
50-
servers = List.of(servers);
94+
var output = MiniYamlDocParser.parse(values);
95+
var itemList = output.get(path);
96+
if (!(itemList instanceof List<?>)) {
97+
itemList = List.of(itemList);
5198
}
52-
((List) servers)
99+
((List) itemList)
53100
.forEach(
54101
it -> {
55102
if (it instanceof Map<?, ?> hash) {
56-
var server = new Server();
57-
server.setDescription((String) hash.get("description"));
58-
server.setUrl((String) hash.get("url"));
59-
result.add(server);
103+
result.add(mapper.apply(hash));
60104
}
61105
});
62106
}

modules/jooby-openapi/src/main/java/io/jooby/openapi/OpenAPIGenerator.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ public String toString(OpenAPIGenerator tool, OpenAPI result) {
168168
info.setExtensions(doc.getExtensions());
169169
}
170170
doc.getServers().forEach(openapi::addServersItem);
171+
doc.getContact().forEach(info::setContact);
172+
doc.getLicense().forEach(info::setLicense);
171173
});
172174
}
173175

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ public void shouldGenerateDoc(OpenAPIResult result) {
1919
+ "info:\n"
2020
+ " title: Library API.\n"
2121
+ " description: \"Available data: Books and authors.\"\n"
22+
+ " contact:\n"
23+
+ " name: Jooby\n"
24+
+ " url: https://jooby.io\n"
25+
+ " email: [email protected]\n"
26+
+ " license:\n"
27+
+ " name: Apache\n"
28+
+ " url: https://jooby.io/LICENSE\n"
2229
+ " version: 4.0.0\n"
2330
+ " x-logo:\n"
2431
+ " url: https://redocly.github.io/redoc/museum-logo.png\n"

0 commit comments

Comments
 (0)