Skip to content

Commit 946286c

Browse files
committed
open-api: javadoc for lambda/script routes
- support method references
1 parent 6e7814f commit 946286c

File tree

11 files changed

+353
-49
lines changed

11 files changed

+353
-49
lines changed

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,20 @@ public String getName() {
218218
TokenTypes.RECORD_DEF)))
219219
.map(this::getSimpleName)
220220
.toList();
221-
var packageScope =
221+
222+
return Stream.concat(Stream.of(getPackage()), classScope.stream())
223+
.collect(Collectors.joining("."));
224+
}
225+
226+
public String getPackage() {
227+
return String.join(
228+
".",
222229
backward(node)
223230
.filter(tokens(TokenTypes.COMPILATION_UNIT))
224231
.findFirst()
225232
.flatMap(it -> tree(it).filter(tokens(TokenTypes.PACKAGE_DEF)).findFirst())
226233
.map(it -> tree(it).filter(tokens(TokenTypes.IDENT)).map(DetailAST::getText).toList())
227-
.orElse(List.of());
228-
return Stream.concat(packageScope.stream(), classScope.stream())
229-
.collect(Collectors.joining("."));
234+
.orElse(List.of()));
230235
}
231236

232237
public boolean isRecord() {

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

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ static DetailNode toJavaDocNode(DetailAST node) {
4747
: new JavadocDetailNodeParser().parseJavadocAsDetailNode(node).getTree();
4848
}
4949

50+
public DetailAST getNode() {
51+
return node;
52+
}
53+
5054
public Map<String, Object> getExtensions() {
5155
return extensions;
5256
}
@@ -96,25 +100,29 @@ public String getText() {
96100

97101
protected static String getText(List<DetailNode> nodes, boolean stripLeading) {
98102
var builder = new StringBuilder();
103+
var visited = new HashSet<DetailNode>();
99104
for (var node : nodes) {
100-
if (node.getType() == JavadocTokenTypes.TEXT) {
101-
var text = node.getText();
102-
if (stripLeading && Character.isWhitespace(text.charAt(0))) {
103-
builder.append(' ').append(text.stripLeading());
104-
} else {
105-
builder.append(text);
106-
}
107-
} else if (node.getType() == JavadocTokenTypes.NEWLINE) {
108-
var next = JavadocUtil.getNextSibling(node);
109-
if (next != null && next.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
110-
builder.append(next.getText());
105+
if (visited.add(node)) {
106+
if (node.getType() == JavadocTokenTypes.TEXT) {
107+
var text = node.getText();
108+
if (stripLeading && Character.isWhitespace(text.charAt(0))) {
109+
builder.append(' ').append(text.stripLeading());
110+
} else {
111+
builder.append(text);
112+
}
113+
} else if (node.getType() == JavadocTokenTypes.NEWLINE) {
114+
var next = JavadocUtil.getNextSibling(node);
115+
if (next != null && next.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
116+
builder.append(next.getText());
117+
visited.add(next);
118+
}
111119
}
112120
}
113121
}
114-
return builder.isEmpty() ? null : builder.toString().trim();
122+
return builder.isEmpty() ? null : builder.toString().trim().replaceAll("\\s+", " ");
115123
}
116124

117-
protected String toString(DetailNode node) {
125+
protected static String toString(DetailNode node) {
118126
return DetailNodeTreeStringPrinter.printTree(node, "", "");
119127
}
120128

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

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static io.jooby.SneakyThrows.throwingFunction;
1010
import static io.jooby.internal.openapi.javadoc.JavaDocSupport.*;
1111
import static io.jooby.internal.openapi.javadoc.JavaDocSupport.tokens;
12+
import static java.util.Optional.ofNullable;
1213

1314
import java.io.File;
1415
import java.nio.file.Files;
@@ -18,6 +19,7 @@
1819
import java.util.concurrent.atomic.AtomicInteger;
1920
import java.util.function.BiConsumer;
2021
import java.util.function.Predicate;
22+
import java.util.stream.Collectors;
2123

2224
import com.puppycrawl.tools.checkstyle.JavaParser;
2325
import com.puppycrawl.tools.checkstyle.api.DetailAST;
@@ -26,6 +28,7 @@
2628
import io.jooby.Router;
2729

2830
public class JavaDocParser {
31+
private record ScriptRef(String operationId, DetailAST comment) {}
2932

3033
private final List<Path> baseDir;
3134
private final Map<Path, DetailAST> cache = new HashMap<>();
@@ -39,7 +42,7 @@ public JavaDocParser(List<Path> baseDir) {
3942
}
4043

4144
public Optional<ClassDoc> parse(String typeName) {
42-
return Optional.ofNullable(traverse(resolveType(typeName)).get(typeName));
45+
return ofNullable(traverse(resolveType(typeName)).get(typeName));
4346
}
4447

4548
public Map<String, ClassDoc> traverse(DetailAST tree) {
@@ -76,7 +79,7 @@ public Map<String, ClassDoc> traverse(DetailAST tree) {
7679
}
7780
});
7881
// Script routes
79-
scripts(scope, null, null, new HashSet<>(), classDoc);
82+
scripts(scope, classDoc, null, null, new HashSet<>());
8083

8184
if (counter.get() > 0) {
8285
classes.put(classDoc.getName(), classDoc);
@@ -86,7 +89,7 @@ public Map<String, ClassDoc> traverse(DetailAST tree) {
8689
}
8790

8891
private void scripts(
89-
DetailAST scope, PathDoc pathDoc, String prefix, Set<DetailAST> visited, ClassDoc classDoc) {
92+
DetailAST scope, ClassDoc classDoc, PathDoc pathDoc, String prefix, Set<DetailAST> visited) {
9093
for (var script : tree(scope).filter(tokens(TokenTypes.METHOD_CALL)).toList()) {
9194
if (visited.add(script)) {
9295
// Test for HTTP method name
@@ -107,13 +110,17 @@ private void scripts(
107110
pathLiteral(script)
108111
.ifPresent(
109112
pattern -> {
113+
var resolvedComment = resolveScriptComment(classDoc, script, scriptComment);
110114
var scriptDoc =
111115
new ScriptDoc(
112116
this,
113117
callName.toUpperCase(),
114118
computePath(prefix, pattern),
115119
script,
116-
scriptComment);
120+
resolvedComment.comment);
121+
if (resolvedComment.operationId() != null) {
122+
scriptDoc.setOperationId(resolvedComment.operationId());
123+
}
117124
scriptDoc.setPath(pathDoc);
118125
classDoc.addScript(scriptDoc);
119126
});
@@ -123,16 +130,141 @@ private void scripts(
123130
path -> {
124131
scripts(
125132
script,
133+
classDoc,
126134
new PathDoc(this, script, scriptComment),
127135
computePath(prefix, path),
128-
visited,
129-
classDoc);
136+
visited);
130137
});
131138
}
132139
}
133140
}
134141
}
135142

143+
/**
144+
* get("/reference", this::findPetById); post("/static-reference",
145+
* javadoc.input.LambdaRefApp::staticFindPetById); put("/external-reference",
146+
* RequestHandler::external); get("/external-subPackage-reference",
147+
* SubPackageHandler::subPackage);
148+
*
149+
* @param classDoc
150+
* @param script
151+
* @param defaultComment
152+
* @return
153+
*/
154+
private ScriptRef resolveScriptComment(
155+
ClassDoc classDoc, DetailAST script, DetailAST defaultComment) {
156+
// ELIST -> LAMBDA (children)
157+
// ELIST -> EXPR -> METHOD_REF (tree)
158+
return children(script)
159+
.filter(tokens(TokenTypes.ELIST))
160+
.findFirst()
161+
.map(
162+
statementList ->
163+
children(statementList)
164+
.filter(tokens(TokenTypes.LAMBDA))
165+
.findFirst()
166+
.map(lambda -> new ScriptRef(null, defaultComment))
167+
.orElseGet(
168+
() ->
169+
tree(statementList)
170+
.filter(tokens(TokenTypes.METHOD_REF))
171+
.findFirst()
172+
.flatMap(
173+
ref -> ofNullable(resolveFromMethodRef(classDoc, script, ref)))
174+
.orElseGet(() -> new ScriptRef(null, defaultComment))))
175+
.orElseGet(() -> new ScriptRef(null, defaultComment));
176+
}
177+
178+
private ScriptRef resolveFromMethodRef(ClassDoc classDoc, DetailAST script, DetailAST methodRef) {
179+
var referenceOwner = getTypeName(methodRef);
180+
DetailAST scope = null;
181+
String className;
182+
if (referenceOwner.equals("this")) {
183+
scope = classDoc.getNode();
184+
className = classDoc.getName();
185+
} else {
186+
// resolve className
187+
className = toQualifiedName(classDoc, referenceOwner);
188+
scope = resolveType(className);
189+
if (scope == JavaDocNode.EMPTY_AST) {
190+
// not found
191+
return null;
192+
}
193+
}
194+
var methodName =
195+
children(methodRef).filter(tokens(TokenTypes.IDENT)).toList().getLast().getText();
196+
var method =
197+
tree(scope)
198+
.filter(tokens(TokenTypes.METHOD_DEF))
199+
.filter(
200+
it ->
201+
children(it)
202+
.filter(tokens(TokenTypes.IDENT))
203+
.findFirst()
204+
.filter(e -> e.getText().equals(methodName))
205+
.isPresent())
206+
// One Argument
207+
.filter(it -> tree(it).filter(tokens(TokenTypes.PARAMETER_DEF)).count() == 1)
208+
// Context Type
209+
.filter(
210+
it ->
211+
tree(it)
212+
.filter(tokens(TokenTypes.PARAMETER_DEF))
213+
.findFirst()
214+
.flatMap(p -> children(p).filter(tokens(TokenTypes.TYPE)).findFirst())
215+
.filter(type -> getTypeName(type).equals("Context"))
216+
.isPresent())
217+
.findFirst()
218+
.orElseThrow(
219+
() ->
220+
new IllegalArgumentException(
221+
"No method found: " + className + "." + methodName));
222+
return children(method)
223+
.filter(tokens(TokenTypes.MODIFIERS))
224+
.findFirst()
225+
.flatMap(it -> children(it).filter(tokens(TokenTypes.BLOCK_COMMENT_BEGIN)).findFirst())
226+
.map(comment -> new ScriptRef(methodName, comment))
227+
.orElseGet(() -> new ScriptRef(null, JavaDocNode.EMPTY_AST));
228+
}
229+
230+
private static String getTypeName(DetailAST methodRef) {
231+
var referenceOwner =
232+
tree(methodRef.getFirstChild())
233+
.filter(tokens(TokenTypes.DOT).negate())
234+
.map(DetailAST::getText)
235+
.collect(Collectors.joining("."));
236+
return referenceOwner;
237+
}
238+
239+
private static String toQualifiedName(ClassDoc classDoc, String referenceOwner) {
240+
var className = referenceOwner;
241+
if (!className.contains(".")) {
242+
if (!classDoc.getSimpleName().equals(className)) {
243+
var cu =
244+
backward(classDoc.getNode())
245+
.filter(tokens(TokenTypes.COMPILATION_UNIT))
246+
.findFirst()
247+
.orElseThrow(
248+
() ->
249+
new IllegalArgumentException(
250+
"No compilation unit found: " + referenceOwner));
251+
className =
252+
children(cu)
253+
.filter(tokens(TokenTypes.IMPORT))
254+
.map(
255+
it ->
256+
tree(it.getFirstChild())
257+
.filter(tokens(TokenTypes.DOT).negate())
258+
.map(DetailAST::getText)
259+
.collect(Collectors.joining(".")))
260+
.filter(qualifiedName -> qualifiedName.endsWith("." + referenceOwner))
261+
.findFirst()
262+
.orElseGet(() -> String.join(".", classDoc.getPackage(), referenceOwner));
263+
}
264+
}
265+
return className;
266+
}
267+
136268
/**
137269
* ELIST -> EXPR -> STRING_LITERAL
138270
*

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ public String getOperationId() {
3939
return operationId;
4040
}
4141

42+
public void setOperationId(String operationId) {
43+
this.operationId = operationId;
44+
}
45+
4246
public List<String> getParameterNames() {
4347
var result = new ArrayList<String>();
4448
var index = 0;

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

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.ArrayList;
99
import java.util.List;
1010

11+
import io.jooby.Context;
1112
import io.jooby.Jooby;
1213

1314
/**
@@ -39,23 +40,7 @@ public class ScriptLibrary extends Jooby {
3940
path(
4041
"/api/library",
4142
() -> {
42-
/*
43-
* Find a book by isbn.
44-
*
45-
* @param isbn Book isbn. Like IK-1900.
46-
* @return A matching book.
47-
* @throws NotFoundException <code>404</code> If a book doesn't exist.
48-
* @throws BadRequestException <code>400</code> For bad ISBN code.
49-
* @tag Book
50-
* @tag Author
51-
* @operationId bookByIsbn
52-
*/
53-
get(
54-
"/{isbn}",
55-
ctx -> {
56-
var isbn = ctx.path("isbn").value();
57-
return new Book();
58-
});
43+
get("/{isbn}", this::bookByIsbn);
5944

6045
/*
6146
* Author by Id.
@@ -108,4 +93,20 @@ public class ScriptLibrary extends Jooby {
10893
});
10994
});
11095
}
96+
97+
/*
98+
* Find a book by isbn.
99+
*
100+
* @param isbn Book isbn. Like IK-1900.
101+
* @return A matching book.
102+
* @throws NotFoundException <code>404</code> If a book doesn't exist.
103+
* @throws BadRequestException <code>400</code> For bad ISBN code.
104+
* @tag Book
105+
* @tag Author
106+
* @operationId bookByIsbn
107+
*/
108+
private Book bookByIsbn(Context ctx) {
109+
var isbn = ctx.path("isbn").value();
110+
return new Book();
111+
}
111112
}

0 commit comments

Comments
 (0)