Skip to content

Commit bfd4634

Browse files
committed
open-api: add @tag javadoc support
- ref #3729
1 parent 826c011 commit bfd4634

File tree

6 files changed

+146
-43
lines changed

6 files changed

+146
-43
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.swagger.v3.oas.models.media.ObjectSchema;
3434
import io.swagger.v3.oas.models.media.Schema;
3535
import io.swagger.v3.oas.models.parameters.Parameter;
36+
import io.swagger.v3.oas.models.tags.Tag;
3637
import jakarta.inject.Named;
3738
import jakarta.inject.Provider;
3839

@@ -279,6 +280,7 @@ public static List<OperationExt> parse(ParserContext ctx, String prefix, Type ty
279280
doc -> {
280281
operationExt.setPathDescription(doc.getDescription());
281282
operationExt.setPathSummary(doc.getSummary());
283+
tags(doc.getTags()).forEach(operationExt::addTag);
282284
if (!doc.getExtensions().isEmpty()) {
283285
operationExt.setPathExtensions(doc.getExtensions());
284286
}
@@ -296,6 +298,7 @@ public static List<OperationExt> parse(ParserContext ctx, String prefix, Type ty
296298
if (!methodDoc.getExtensions().isEmpty()) {
297299
operationExt.setExtensions(methodDoc.getExtensions());
298300
}
301+
tags(methodDoc.getTags()).forEach(operationExt::addTag);
299302
// Parameters
300303
for (var parameterName : parameterNames) {
301304
var paramExt =
@@ -343,6 +346,17 @@ public static List<OperationExt> parse(ParserContext ctx, String prefix, Type ty
343346
return result;
344347
}
345348

349+
private static List<Tag> tags(Map<String, String> tags) {
350+
List<Tag> result = new ArrayList<>();
351+
for (var tagNode : tags.entrySet()) {
352+
var tag = new Tag();
353+
tag.setName(tagNode.getKey());
354+
tag.setDescription(tagNode.getValue());
355+
result.add(tag);
356+
}
357+
return result;
358+
}
359+
346360
private static Map<String, MethodNode> methods(ParserContext ctx, ClassNode node) {
347361
Map<String, MethodNode> methods = new LinkedHashMap<>();
348362
if (node.superName != null && !node.superName.equals(TypeFactory.OBJECT.getInternalName())) {

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

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,27 @@
66
package io.jooby.internal.openapi.javadoc;
77

88
import static io.jooby.internal.openapi.javadoc.JavaDocSupport.*;
9+
import static io.jooby.internal.openapi.javadoc.JavaDocSupport.javadocToken;
910

1011
import java.util.*;
1112
import java.util.function.Predicate;
1213

13-
import com.fasterxml.jackson.core.JsonProcessingException;
1414
import com.puppycrawl.tools.checkstyle.DetailNodeTreeStringPrinter;
1515
import com.puppycrawl.tools.checkstyle.JavadocDetailNodeParser;
1616
import com.puppycrawl.tools.checkstyle.api.DetailAST;
1717
import com.puppycrawl.tools.checkstyle.api.DetailNode;
1818
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
1919
import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
20-
import io.swagger.util.Yaml;
2120

2221
public class JavaDocNode {
2322
private static final Predicate<DetailNode> JAVADOC_TAG =
24-
JavaDocSupport.javadocToken(JavadocTokenTypes.JAVADOC_TAG);
23+
javadocToken(JavadocTokenTypes.JAVADOC_TAG);
2524

2625
protected final JavaDocParser context;
2726
protected final DetailAST node;
2827
protected final DetailNode javadoc;
2928
private final Map<String, Object> extensions;
29+
private final Map<String, String> tags;
3030

3131
public JavaDocNode(JavaDocParser ctx, DetailAST node, DetailAST comment) {
3232
this(ctx, node, toJavaDocNode(comment));
@@ -38,14 +38,55 @@ protected JavaDocNode(JavaDocParser ctx, DetailAST node, DetailNode javadoc) {
3838
this.javadoc = javadoc;
3939
if (this.javadoc != EMPTY_NODE) {
4040
this.extensions = parseExtensions(this.javadoc);
41+
this.tags = parseTags(this.javadoc);
4142
} else {
4243
this.extensions = Map.of();
44+
this.tags = Map.of();
4345
}
4446
}
4547

48+
private Map<String, String> parseTags(DetailNode node) {
49+
var result = new LinkedHashMap<String, String>();
50+
for (var docTag : tree(node).filter(JAVADOC_TAG).toList()) {
51+
var tag =
52+
tree(docTag)
53+
.filter(
54+
javadocToken(JavadocTokenTypes.CUSTOM_NAME)
55+
.and(it -> it.getText().equals("@tag")))
56+
.findFirst()
57+
.orElse(null);
58+
if (tag != null) {
59+
var tagText =
60+
tree(docTag)
61+
.filter(javadocToken(JavadocTokenTypes.DESCRIPTION))
62+
.findFirst()
63+
.map(it -> getText(List.of(it.getChildren()), false))
64+
.orElse(null);
65+
if (tagText != null) {
66+
var dot = tagText.indexOf(".");
67+
var tagName = tagText;
68+
String tagDescription = null;
69+
if (dot > 0) {
70+
tagName = tagText.substring(0, dot);
71+
if (dot + 1 < tagText.length()) {
72+
tagDescription = tagText.substring(dot + 1).trim();
73+
if (tagDescription.isBlank()) {
74+
tagDescription = null;
75+
}
76+
}
77+
}
78+
if (!tagName.trim().isEmpty()) {
79+
result.put(tagName, tagDescription);
80+
}
81+
}
82+
}
83+
}
84+
return result;
85+
}
86+
4687
private Map<String, Object> parseExtensions(DetailNode node) {
4788
var values = new ArrayList<String>();
48-
for (var tag : tree(node).filter(javadocToken(JavadocTokenTypes.JAVADOC_TAG)).toList()) {
89+
for (var tag : tree(node).filter(JAVADOC_TAG).toList()) {
4990
var extension =
5091
tree(tag)
5192
.filter(
@@ -105,6 +146,10 @@ public String getSummary() {
105146
return string.isEmpty() ? null : string;
106147
}
107148

149+
public Map<String, String> getTags() {
150+
return tags;
151+
}
152+
108153
public String getDescription() {
109154
var text = getText();
110155
var summary = getSummary();
@@ -257,16 +302,4 @@ public boolean hasChildren() {
257302
return false;
258303
}
259304
};
260-
261-
public static void main(String[] args) throws JsonProcessingException {
262-
var badges =
263-
Yaml.mapper()
264-
.readValue(
265-
"x-badges:\n"
266-
+ " - name: 'Beta'\n"
267-
+ " position: before\n"
268-
+ " color: purple",
269-
Map.class);
270-
System.out.println(badges);
271-
}
272305
}

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,16 @@ public String toString(OpenAPIGenerator tool, OpenAPI result) {
221221
Optional.ofNullable(operation.getPathExtensions()).ifPresent(pathItem::setExtensions);
222222

223223
// global tags
224-
operation.getGlobalTags().forEach(tag -> globalTags.put(tag.getName(), tag));
224+
operation
225+
.getGlobalTags()
226+
.forEach(
227+
tag -> {
228+
if (tag.getDescription() != null || tag.getExtensions() != null) {
229+
globalTags.put(tag.getName(), tag);
230+
}
231+
});
225232
}
226-
globalTags
227-
.values()
228-
.forEach(
229-
tag -> {
230-
if (tag.getDescription() != null || tag.getExtensions() != null) {
231-
openapi.addTagsItem(tag);
232-
}
233-
});
233+
globalTags.values().forEach(openapi::addTagsItem);
234234
openapi.setOperations(operations);
235235
openapi.setPaths(paths);
236236

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

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,21 @@ public void shouldGenerateDoc(OpenAPIResult result) {
2323
+ " x-logo:\n"
2424
+ " url: https://redocly.github.io/redoc/museum-logo.png\n"
2525
+ " altText: Museum logo\n"
26+
+ "tags:\n"
27+
+ "- name: Library\n"
28+
+ " description: Access to all books.\n"
29+
+ "- name: Author\n"
30+
+ " description: Oxxx\n"
2631
+ "paths:\n"
2732
+ " /api/library/{isbn}:\n"
2833
+ " summary: Library API.\n"
2934
+ " description: \"Contains all operations for creating, updating and fetching"
3035
+ " books.\"\n"
3136
+ " get:\n"
37+
+ " tags:\n"
38+
+ " - Library\n"
39+
+ " - Book\n"
40+
+ " - Author\n"
3241
+ " summary: Find a book by isbn.\n"
3342
+ " operationId: bookByIsbn\n"
3443
+ " parameters:\n"
@@ -49,11 +58,37 @@ public void shouldGenerateDoc(OpenAPIResult result) {
4958
+ " description: \"Not Found: If a book doesn't exist.\"\n"
5059
+ " \"400\":\n"
5160
+ " description: \"Bad Request: For bad ISBN code.\"\n"
61+
+ " /api/library/{id}:\n"
62+
+ " summary: Library API.\n"
63+
+ " description: \"Contains all operations for creating, updating and fetching"
64+
+ " books.\"\n"
65+
+ " get:\n"
66+
+ " tags:\n"
67+
+ " - Library\n"
68+
+ " - Author\n"
69+
+ " summary: Author by Id.\n"
70+
+ " operationId: author\n"
71+
+ " parameters:\n"
72+
+ " - name: id\n"
73+
+ " in: path\n"
74+
+ " description: ID.\n"
75+
+ " required: true\n"
76+
+ " schema:\n"
77+
+ " type: string\n"
78+
+ " responses:\n"
79+
+ " \"200\":\n"
80+
+ " description: An author\n"
81+
+ " content:\n"
82+
+ " application/json:\n"
83+
+ " schema:\n"
84+
+ " $ref: \"#/components/schemas/Author\"\n"
5285
+ " /api/library:\n"
5386
+ " summary: Library API.\n"
5487
+ " description: \"Contains all operations for creating, updating and fetching"
5588
+ " books.\"\n"
5689
+ " get:\n"
90+
+ " tags:\n"
91+
+ " - Library\n"
5792
+ " summary: Query books.\n"
5893
+ " operationId: query\n"
5994
+ " parameters:\n"
@@ -86,6 +121,9 @@ public void shouldGenerateDoc(OpenAPIResult result) {
86121
+ " position: before\n"
87122
+ " color: purple\n"
88123
+ " post:\n"
124+
+ " tags:\n"
125+
+ " - Library\n"
126+
+ " - Author\n"
89127
+ " summary: Creates a new book.\n"
90128
+ " description: Book can be created or updated.\n"
91129
+ " operationId: createBook\n"
@@ -105,6 +143,23 @@ public void shouldGenerateDoc(OpenAPIResult result) {
105143
+ " $ref: \"#/components/schemas/Book\"\n"
106144
+ "components:\n"
107145
+ " schemas:\n"
146+
+ " Author:\n"
147+
+ " type: object\n"
148+
+ " properties:\n"
149+
+ " ssn:\n"
150+
+ " type: string\n"
151+
+ " description: Social security number.\n"
152+
+ " name:\n"
153+
+ " type: string\n"
154+
+ " description: Author's name.\n"
155+
+ " address:\n"
156+
+ " $ref: \"#/components/schemas/Address\"\n"
157+
+ " books:\n"
158+
+ " uniqueItems: true\n"
159+
+ " type: array\n"
160+
+ " description: Published books.\n"
161+
+ " items:\n"
162+
+ " $ref: \"#/components/schemas/Book\"\n"
108163
+ " BookQuery:\n"
109164
+ " type: object\n"
110165
+ " properties:\n"
@@ -165,24 +220,7 @@ public void shouldGenerateDoc(OpenAPIResult result) {
165220
+ " type: array\n"
166221
+ " items:\n"
167222
+ " $ref: \"#/components/schemas/Author\"\n"
168-
+ " description: Book model.\n"
169-
+ " Author:\n"
170-
+ " type: object\n"
171-
+ " properties:\n"
172-
+ " ssn:\n"
173-
+ " type: string\n"
174-
+ " description: Social security number.\n"
175-
+ " name:\n"
176-
+ " type: string\n"
177-
+ " description: Author's name.\n"
178-
+ " address:\n"
179-
+ " $ref: \"#/components/schemas/Address\"\n"
180-
+ " books:\n"
181-
+ " uniqueItems: true\n"
182-
+ " type: array\n"
183-
+ " description: Published books.\n"
184-
+ " items:\n"
185-
+ " $ref: \"#/components/schemas/Book\"\n",
223+
+ " description: Book model.\n",
186224
result.toYaml());
187225
}
188226
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* Library API.
1616
*
1717
* <p>Contains all operations for creating, updating and fetching books.
18+
*
19+
* @tag Library. Access to all books.
1820
*/
1921
@Path("/api/library")
2022
public class LibraryApi {
@@ -26,12 +28,26 @@ public class LibraryApi {
2628
* @return A matching book.
2729
* @throws NotFoundException <code>404</code> If a book doesn't exist.
2830
* @throws BadRequestException <code>400</code> For bad ISBN code.
31+
* @tag Book
32+
* @tag Author
2933
*/
3034
@GET("/{isbn}")
3135
public Book bookByIsbn(@PathParam String isbn) throws NotFoundException, BadRequestException {
3236
return new Book();
3337
}
3438

39+
/**
40+
* Author by Id.
41+
*
42+
* @param id ID.
43+
* @return An author
44+
* @tag Author. Oxxx
45+
*/
46+
@GET("/{id}")
47+
public Author author(@PathParam String id) {
48+
return new Author();
49+
}
50+
3551
/**
3652
* Query books.
3753
*
@@ -53,6 +69,7 @@ public List<Book> query(@QueryParam BookQuery query) {
5369
*
5470
* @param book Book to create.
5571
* @return Saved book.
72+
* @tag Author
5673
*/
5774
@POST
5875
public Book createBook(Book book) {

modules/jooby-openapi/src/test/java/javadoc/input/ApiDoc.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* @x-badges.name Beta
2222
* @x-badges.position before
2323
* @x-badges.color purple
24+
* @tag ApiTag
2425
*/
2526
@Path("/api")
2627
public class ApiDoc {

0 commit comments

Comments
 (0)