Skip to content

Commit 6ff90e2

Browse files
committed
openapi: parse javadoc from enum/record
1 parent 6606ca2 commit 6ff90e2

8 files changed

Lines changed: 193 additions & 52 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public JavaDocNode(JavaDocContext ctx, DetailAST node) {
2525
this.javadoc = toJavaDocNode(node);
2626
}
2727

28-
private static DetailNode toJavaDocNode(DetailAST node) {
28+
static DetailNode toJavaDocNode(DetailAST node) {
2929
return new JavadocDetailNodeParser().parseJavadocAsDetailNode(node).getTree();
3030
}
3131

@@ -50,7 +50,7 @@ protected String getText(List<DetailNode> nodes, boolean stripLeading) {
5050
}
5151
}
5252
}
53-
return builder.toString().trim();
53+
return builder.isEmpty() ? null : builder.toString().trim();
5454
}
5555

5656
@Override

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,22 @@
1414
import com.puppycrawl.tools.checkstyle.api.DetailAST;
1515
import com.puppycrawl.tools.checkstyle.api.DetailNode;
1616
import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
17-
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
1817

1918
public final class JavaDocSupport {
2019

2120
public static Predicate<DetailAST> tokens(Integer... types) {
2221
return tokens(Set.of(types));
2322
}
2423

25-
public static Predicate<DetailAST> imaginary() {
26-
return it -> it.getText().equals(TokenUtil.getTokenName(it.getType()));
24+
private static Predicate<DetailAST> tokens(Set<Integer> types) {
25+
return it -> types.contains(it.getType());
2726
}
2827

29-
private static Predicate<DetailAST> tokens(Set<Integer> types) {
28+
public static Predicate<DetailNode> javadocToken(Integer... types) {
29+
return javadocToken(Set.of(types));
30+
}
31+
32+
private static Predicate<DetailNode> javadocToken(Set<Integer> types) {
3033
return it -> types.contains(it.getType());
3134
}
3235

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

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
import java.nio.file.Paths;
1313
import java.util.ArrayList;
1414
import java.util.List;
15+
import java.util.Optional;
1516
import java.util.stream.Stream;
1617

1718
import com.puppycrawl.tools.checkstyle.api.DetailAST;
18-
import com.puppycrawl.tools.checkstyle.api.DetailNode;
1919
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
2020
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
2121

@@ -70,7 +70,6 @@ public String getParameterDoc(String name) {
7070
}
7171

7272
public String getParameterDoc(String name, String in) {
73-
DetailNode javadoc = this.javadoc;
7473
if (in != null) {
7574
var tree = context.resolve(toJavaPath(in));
7675
if (tree == JavaDocContext.NULL) {
@@ -104,21 +103,73 @@ public String getParameterDoc(String name, String in) {
104103
.orElse(null);
105104
}
106105

106+
public String getReturnDoc() {
107+
return tree(javadoc)
108+
.filter(javadocToken(JavadocTokenTypes.RETURN_LITERAL))
109+
.findFirst()
110+
.flatMap(
111+
it ->
112+
tree(it.getParent())
113+
.filter(javadocToken(JavadocTokenTypes.DESCRIPTION))
114+
.findFirst())
115+
.map(it -> getText(tree(it).toList(), true))
116+
.orElse(null);
117+
}
118+
107119
private Path toJavaPath(String in) {
108120
var segments = in.split("\\.");
109121
segments[segments.length - 1] = segments[segments.length - 1] + ".java";
110122
return Paths.get(String.join(File.separator, segments));
111123
}
112124

113125
private String getPropertyDoc(DetailAST bean, String name) {
114-
var comment = commentFromGetter(bean, name);
115-
if (comment == null) {
116-
comment = commentFromField(bean, name);
126+
String comment;
127+
var isRecord = tree(bean).anyMatch(tokens(TokenTypes.RECORD_DEF));
128+
if (isRecord) {
129+
comment = commentFromRecord(bean, name);
130+
} else {
131+
comment = commentFromGetter(bean, name);
132+
if (comment == null) {
133+
comment = commentFromField(bean, name);
134+
}
117135
}
118-
return comment == null ? null : new JavaDocNode(context, comment).getText();
136+
return comment;
119137
}
120138

121-
private DetailAST commentFromGetter(DetailAST bean, String name) {
139+
private String commentFromRecord(DetailAST bean, String name) {
140+
var commentNode =
141+
tree(bean)
142+
.filter(tokens(TokenTypes.RECORD_DEF))
143+
.findFirst()
144+
.flatMap(it -> Optional.ofNullable(commentFromMember(it)))
145+
.map(JavaDocNode::toJavaDocNode)
146+
.orElse(null);
147+
if (commentNode == null) {
148+
return null;
149+
}
150+
151+
for (var tag : tree(commentNode).filter(javadocToken(JavadocTokenTypes.JAVADOC_TAG)).toList()) {
152+
var isParam = tree(tag).anyMatch(javadocToken(JavadocTokenTypes.PARAM_LITERAL));
153+
var matchesName =
154+
tree(tag)
155+
.filter(javadocToken(JavadocTokenTypes.PARAMETER_NAME))
156+
.findFirst()
157+
.filter(it -> it.getText().equals(name))
158+
.isPresent();
159+
if (isParam && matchesName) {
160+
return getText(
161+
tree(tag)
162+
.filter(javadocToken(JavadocTokenTypes.DESCRIPTION))
163+
.flatMap(it -> Stream.of(it.getChildren()))
164+
.toList(),
165+
true);
166+
}
167+
}
168+
169+
return null;
170+
}
171+
172+
private String commentFromGetter(DetailAST bean, String name) {
122173
var methods = JavaDocSupport.forward(bean).filter(tokens(TokenTypes.METHOD_DEF)).toList();
123174
var getterName = "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
124175
for (var method : methods) {
@@ -133,14 +184,18 @@ private DetailAST commentFromGetter(DetailAST bean, String name) {
133184
if (noArgs && isPublic && (methodName.equals(getterName) || methodName.equals(name))) {
134185
var comment = commentFromMember(method);
135186
if (comment != null) {
136-
return comment;
187+
return textFromComment(comment);
137188
}
138189
}
139190
}
140191
return null;
141192
}
142193

143-
private DetailAST commentFromField(DetailAST bean, String name) {
194+
private String textFromComment(DetailAST comment) {
195+
return comment == null ? null : new JavaDocNode(context, comment).getText();
196+
}
197+
198+
private String commentFromField(DetailAST bean, String name) {
144199
for (var field :
145200
JavaDocSupport.forward(bean).filter(tokens(TokenTypes.VARIABLE_DEF)).toList()) {
146201
var isInstance = tree(field).noneMatch(tokens(TokenTypes.LITERAL_STATIC));
@@ -153,7 +208,7 @@ private DetailAST commentFromField(DetailAST bean, String name) {
153208
if (isInstance && fieldName.equals(name)) {
154209
var comment = commentFromMember(field);
155210
if (comment != null) {
156-
return comment;
211+
return textFromComment(comment);
157212
}
158213
}
159214
}
@@ -163,11 +218,10 @@ private DetailAST commentFromField(DetailAST bean, String name) {
163218
private static DetailAST commentFromMember(DetailAST member) {
164219
var modifiers = tree(member).filter(tokens(TokenTypes.MODIFIERS)).findFirst().orElse(null);
165220
if (modifiers != null) {
166-
var comment =
167-
tree(modifiers).filter(tokens(TokenTypes.BLOCK_COMMENT_BEGIN)).findFirst().orElse(null);
168-
if (comment != null) {
169-
return comment;
170-
}
221+
return tree(modifiers)
222+
.filter(tokens(TokenTypes.BLOCK_COMMENT_BEGIN))
223+
.findFirst()
224+
.orElse(null);
171225
}
172226
return null;
173227
}

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

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.jooby.internal.openapi.javadoc.ClassDoc;
2121
import io.jooby.internal.openapi.javadoc.JavaDocContext;
2222
import io.jooby.internal.openapi.javadoc.JavaDocParser;
23+
import io.jooby.internal.openapi.javadoc.MethodDoc;
2324

2425
public class JavaDocParserTest {
2526

@@ -35,32 +36,57 @@ public void apiDoc() throws Exception {
3536
"Proin sit amet lectus interdum, porta libero quis, fringilla metus. Integer viverra"
3637
+ " ante id vestibulum congue. Nam et tortor at magna tempor congue.",
3738
doc.getDescription());
38-
// throw new UnsupportedOperationException();
39-
var methods = doc.getMethods();
40-
assertEquals(2, methods.size());
41-
assertEquals("hello", methods.get(0).getName());
42-
assertEquals(List.of("name", "age", "list", "str"), methods.get(0).getParameterNames());
43-
assertEquals(
44-
List.of("List", "int", "List", "String"), methods.get(0).getParameterTypes());
45-
//
46-
var method = doc.getMethod("hello", List.of("List", "int", "List", "String"));
47-
assertTrue(method.isPresent());
48-
assertEquals("This is the Hello /endpoint.", method.get().getText());
49-
assertEquals("Person name.", method.get().getParameterDoc("name"));
50-
assertEquals("Person age.", method.get().getParameterDoc("age"));
51-
assertEquals("This line has a break.", method.get().getParameterDoc("list"));
52-
assertEquals("Some string.", method.get().getParameterDoc("str"));
5339

54-
var search = doc.getMethod("search", List.of("QueryBeanDoc"));
55-
assertTrue(search.isPresent());
56-
assertEquals("Search database.", search.get().getText());
57-
assertEquals(
58-
"Filter query. Works like internal filter.",
59-
search.get().getParameterDoc("fq", "javadoc.input.QueryBeanDoc"));
60-
assertEquals(
61-
"Offset, used for paging.",
62-
search.get().getParameterDoc("offset", "javadoc.input.QueryBeanDoc"));
63-
assertNull(search.get().getParameterDoc("limit", "javadoc.input.QueryBeanDoc"));
40+
withMethod(
41+
doc,
42+
"hello",
43+
List.of("List", "int", "List", "String"),
44+
method -> {
45+
assertEquals("This is the Hello /endpoint.", method.getText());
46+
assertEquals("Person name.", method.getParameterDoc("name"));
47+
assertEquals("Person age.", method.getParameterDoc("age"));
48+
assertEquals("This line has a break.", method.getParameterDoc("list"));
49+
assertEquals("Some string.", method.getParameterDoc("str"));
50+
assertEquals("Welcome message 200.", method.getReturnDoc());
51+
});
52+
53+
withMethod(
54+
doc,
55+
"search",
56+
List.of("QueryBeanDoc"),
57+
method -> {
58+
assertEquals("Search database.", method.getText());
59+
assertEquals(
60+
"Filter query. Works like internal filter.",
61+
method.getParameterDoc("fq", "javadoc.input.QueryBeanDoc"));
62+
assertEquals(
63+
"Offset, used for paging.",
64+
method.getParameterDoc("offset", "javadoc.input.QueryBeanDoc"));
65+
assertNull(method.getParameterDoc("limit", "javadoc.input.QueryBeanDoc"));
66+
assertNull(method.getReturnDoc());
67+
});
68+
69+
withMethod(
70+
doc,
71+
"recordBean",
72+
List.of("RecordBeanDoc"),
73+
method -> {
74+
assertEquals("Record database.", method.getText());
75+
assertEquals(
76+
"Person id.", method.getParameterDoc("id", "javadoc.input.RecordBeanDoc"));
77+
assertEquals(
78+
"Person name. Example: edgar.",
79+
method.getParameterDoc("name", "javadoc.input.RecordBeanDoc"));
80+
});
81+
82+
withMethod(
83+
doc,
84+
"enumParam",
85+
List.of("EnumDoc"),
86+
method -> {
87+
assertEquals("Enum database.", method.getText());
88+
assertEquals("Enum doc.", method.getParameterDoc("query"));
89+
});
6490
});
6591
}
6692

@@ -91,4 +117,11 @@ private void withDoc(Path path, Consumer<ClassDoc> consumer) throws Exception {
91117
throw SneakyThrows.propagate(cause);
92118
}
93119
}
120+
121+
private void withMethod(
122+
ClassDoc doc, String name, List<String> types, Consumer<MethodDoc> consumer) {
123+
var method = doc.getMethod(name, types);
124+
assertTrue(method.isPresent());
125+
consumer.accept(method.get());
126+
}
94127
}

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,14 @@
99
import java.nio.file.Paths;
1010

1111
import com.puppycrawl.tools.checkstyle.AstTreeStringPrinter;
12-
import com.puppycrawl.tools.checkstyle.JavaParser;
1312
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
1413

1514
public class PrinteAstTree {
1615
public static void main(String[] args) throws CheckstyleException, IOException {
1716
var baseDir =
1817
Paths.get(System.getProperty("user.dir")).resolve("modules").resolve("jooby-openapi");
19-
var input = Paths.get("src", "test", "java", "javadoc", "input", "QueryBeanDoc.java");
20-
var stringAst =
21-
AstTreeStringPrinter.printFileAst(
22-
baseDir.resolve(input).toFile(), JavaParser.Options.WITH_COMMENTS);
18+
var input = Paths.get("src", "test", "java", "javadoc", "input", "EnumDoc.java");
19+
var stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(baseDir.resolve(input).toFile());
2320
System.out.println(stringAst);
2421
}
2522
}

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public class ApiDoc {
2828
* @param age Person age.
2929
* @param list This line has a break.
3030
* @param str Some <code>string</code>.
31-
* @return Say hello.
31+
* @return Welcome message <code>200</code>.
32+
* @throws NullPointerException One something is null.
3233
*/
3334
@NonNull @GET
3435
public String hello(
@@ -49,4 +50,26 @@ public String hello(
4950
public String search(@QueryParam QueryBeanDoc query) {
5051
return "hello";
5152
}
53+
54+
/**
55+
* Record database.
56+
*
57+
* @param query
58+
* @return
59+
*/
60+
@GET
61+
public String recordBean(@QueryParam RecordBeanDoc query) {
62+
return "hello";
63+
}
64+
65+
/**
66+
* Enum database.
67+
*
68+
* @param query Enum doc.
69+
* @return
70+
*/
71+
@GET
72+
public String enumParam(@QueryParam EnumDoc query) {
73+
return "hello";
74+
}
5275
}
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 javadoc.input;
7+
8+
/** Cras dictum. */
9+
public enum EnumDoc {
10+
/** Foo doc. */
11+
Foo,
12+
13+
/** Bar doc. */
14+
Bar
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 javadoc.input;
7+
8+
import jakarta.validation.constraints.NotEmpty;
9+
10+
/**
11+
* Record documentation.
12+
*
13+
* @param id Person id.
14+
* @param name Person name. Example: edgar.
15+
*/
16+
public record RecordBeanDoc(String id, @NotEmpty String name) {}

0 commit comments

Comments
 (0)