Skip to content

Commit 2f710ec

Browse files
committed
open-api
- script/lambda java doc - support nested path - mimic MVC/controller API - add @operationid parsing
1 parent cc3f76e commit 2f710ec

File tree

16 files changed

+556
-92
lines changed

16 files changed

+556
-92
lines changed

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

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,7 @@ public static List<OperationExt> parse(ParserContext ctx, String prefix, Type ty
277277
.parse(className)
278278
.ifPresent(
279279
doc -> {
280-
operationExt.setPathDescription(doc.getDescription());
281-
operationExt.setPathSummary(doc.getSummary());
282-
doc.getTags().forEach(operationExt::addTag);
283-
if (!doc.getExtensions().isEmpty()) {
284-
operationExt.setPathExtensions(doc.getExtensions());
285-
}
280+
JavaDocSetter.setPath(operationExt, doc);
286281
var parameterNames =
287282
Optional.ofNullable(operationExt.getNode().parameters)
288283
.orElse(List.of())
@@ -291,47 +286,7 @@ public static List<OperationExt> parse(ParserContext ctx, String prefix, Type ty
291286
.toList();
292287
doc.getMethod(operationExt.getOperationId(), parameterNames)
293288
.ifPresent(
294-
methodDoc -> {
295-
operationExt.setSummary(methodDoc.getSummary());
296-
operationExt.setDescription(methodDoc.getDescription());
297-
if (!methodDoc.getExtensions().isEmpty()) {
298-
operationExt.setExtensions(methodDoc.getExtensions());
299-
}
300-
methodDoc.getTags().forEach(operationExt::addTag);
301-
// Parameters
302-
for (var parameterName : parameterNames) {
303-
var paramExt =
304-
operationExt.getParameters().stream()
305-
.filter(p -> p.getName().equals(parameterName))
306-
.findFirst()
307-
.map(ParameterExt.class::cast)
308-
.orElse(null);
309-
var paramDoc = methodDoc.getParameterDoc(parameterName);
310-
if (paramDoc != null) {
311-
if (paramExt == null) {
312-
operationExt.getRequestBody().setDescription(paramDoc);
313-
} else {
314-
paramExt.setDescription(paramDoc);
315-
}
316-
}
317-
}
318-
// return types
319-
var defaultResponse = operationExt.getDefaultResponse();
320-
if (defaultResponse != null) {
321-
defaultResponse.setDescription(methodDoc.getReturnDoc());
322-
}
323-
for (var throwsDoc : methodDoc.getThrows().values()) {
324-
var response =
325-
operationExt.getResponse(
326-
Integer.toString(throwsDoc.getStatusCode().value()));
327-
if (response == null) {
328-
response =
329-
operationExt.addResponse(
330-
Integer.toString(throwsDoc.getStatusCode().value()));
331-
}
332-
response.setDescription(throwsDoc.getText());
333-
}
334-
});
289+
methodDoc -> JavaDocSetter.set(operationExt, methodDoc, parameterNames));
335290
});
336291
} catch (Exception x) {
337292
throw SneakyThrows.propagate(x);
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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;
7+
8+
import java.util.LinkedHashSet;
9+
import java.util.List;
10+
import java.util.Optional;
11+
import java.util.stream.Collectors;
12+
13+
import io.jooby.internal.openapi.javadoc.JavaDocNode;
14+
import io.jooby.internal.openapi.javadoc.MethodDoc;
15+
import io.jooby.internal.openapi.javadoc.ScriptDoc;
16+
import io.swagger.v3.oas.models.parameters.Parameter;
17+
18+
public class JavaDocSetter {
19+
20+
public static void setPath(OperationExt operation, JavaDocNode doc) {
21+
operation.setPathDescription(doc.getDescription());
22+
operation.setPathSummary(doc.getSummary());
23+
doc.getTags().forEach(operation::addTag);
24+
if (!doc.getExtensions().isEmpty()) {
25+
operation.setPathExtensions(doc.getExtensions());
26+
}
27+
}
28+
29+
public static void set(OperationExt operation, ScriptDoc doc) {
30+
var parameters = Optional.ofNullable(operation.getParameters()).orElse(List.of());
31+
var parameterNames = parameters.stream().map(Parameter::getName).collect(Collectors.toList());
32+
if (operation.getRequestBody() != null) {
33+
var javaDocNames = new LinkedHashSet<>(doc.getJavadocParameterNames());
34+
javaDocNames.removeAll(parameterNames);
35+
if (javaDocNames.size() == 1) {
36+
// just add body name on lambda/script routes.
37+
parameterNames.addAll(javaDocNames);
38+
}
39+
}
40+
set(operation, doc, parameterNames);
41+
}
42+
43+
public static void set(OperationExt operation, MethodDoc doc, List<String> parameterNames) {
44+
operation.setOperationId(
45+
Optional.ofNullable(operation.getOperationId()).orElse(doc.getOperationId()));
46+
operation.setSummary(doc.getSummary());
47+
operation.setDescription(doc.getDescription());
48+
if (!doc.getExtensions().isEmpty()) {
49+
operation.setExtensions(doc.getExtensions());
50+
}
51+
doc.getTags().forEach(operation::addTag);
52+
// Parameters
53+
for (var parameterName : parameterNames) {
54+
var paramExt =
55+
operation.getParameters().stream()
56+
.filter(p -> p.getName().equals(parameterName))
57+
.findFirst()
58+
.map(ParameterExt.class::cast)
59+
.orElse(null);
60+
var paramDoc = doc.getParameterDoc(parameterName);
61+
if (paramDoc != null) {
62+
if (paramExt == null) {
63+
var body = operation.getRequestBody();
64+
if (body != null) {
65+
body.setDescription(paramDoc);
66+
}
67+
} else {
68+
paramExt.setDescription(paramDoc);
69+
}
70+
}
71+
}
72+
// return types
73+
var defaultResponse = operation.getDefaultResponse();
74+
if (defaultResponse != null) {
75+
defaultResponse.setDescription(doc.getReturnDoc());
76+
}
77+
for (var throwsDoc : doc.getThrows().values()) {
78+
var response = operation.getResponse(Integer.toString(throwsDoc.getStatusCode().value()));
79+
if (response == null) {
80+
response = operation.addResponse(Integer.toString(throwsDoc.getStatusCode().value()));
81+
}
82+
response.setDescription(throwsDoc.getText());
83+
}
84+
}
85+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ public class RequestBodyExt extends RequestBody {
1515

1616
@JsonIgnore private String contentType = MediaType.JSON;
1717

18+
{
19+
setRequired(true);
20+
}
21+
1822
public String getJavaType() {
1923
return javaType;
2024
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ public List<OperationExt> parse(ParserContext ctx, OpenAPIExt openapi) {
6565
Optional.ofNullable(ctx.getMainClass()).orElse(ctx.getRouter().getClassName());
6666
ClassNode application = ctx.classNode(Type.getObjectType(applicationName.replace(".", "/")));
6767

68+
// javadoc
69+
var javaDoc = ctx.javadoc().parse(ctx.getRouter().getClassName());
70+
for (OperationExt operation : operations) {
71+
// Script/lambda
72+
if (operation.getController() == null) {
73+
javaDoc
74+
.flatMap(doc -> doc.getScript(operation.getMethod(), operation.getPattern()))
75+
.ifPresent(
76+
scriptDoc -> {
77+
if (scriptDoc.getPath() != null) {
78+
JavaDocSetter.setPath(operation, scriptDoc.getPath());
79+
}
80+
JavaDocSetter.set(operation, scriptDoc);
81+
});
82+
}
83+
}
6884
// swagger/openapi:
6985
for (OperationExt operation : operations) {
7086
operation.setApplication(application);

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
public class ClassDoc extends JavaDocNode {
2525
private final Map<String, FieldDoc> fields = new LinkedHashMap<>();
2626
private final Map<String, MethodDoc> methods = new LinkedHashMap<>();
27-
private final Map<String, MethodDoc> scripts = new LinkedHashMap<>();
27+
private final Map<String, ScriptDoc> scripts = new LinkedHashMap<>();
2828
private final List<Server> servers;
2929
private final List<Contact> contact;
3030
private final List<License> license;
@@ -161,8 +161,8 @@ public void addMethod(MethodDoc method) {
161161
this.methods.put(toMethodSignature(method), method);
162162
}
163163

164-
public void addScript(String pattern, MethodDoc method) {
165-
this.scripts.put(pattern, method);
164+
public void addScript(ScriptDoc method) {
165+
this.scripts.put(toScriptSignature(method), method);
166166
}
167167

168168
public void addField(FieldDoc field) {
@@ -177,8 +177,16 @@ public Optional<MethodDoc> getMethod(String name, List<String> parameterNames) {
177177
return Optional.ofNullable(methods.get(toMethodSignature(name, parameterNames)));
178178
}
179179

180-
public Optional<MethodDoc> getScript(String pattern) {
181-
return Optional.ofNullable(scripts.get(pattern));
180+
public Optional<ScriptDoc> getScript(String method, String pattern) {
181+
return Optional.ofNullable(scripts.get(toScriptSignature(method, pattern)));
182+
}
183+
184+
private String toScriptSignature(ScriptDoc method) {
185+
return toScriptSignature(method.getMethod(), method.getPattern());
186+
}
187+
188+
private String toScriptSignature(String method, String pattern) {
189+
return method + "/" + pattern;
182190
}
183191

184192
private String toMethodSignature(MethodDoc method) {

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

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -76,35 +76,7 @@ public Map<String, ClassDoc> traverse(DetailAST tree) {
7676
}
7777
});
7878
// Script routes
79-
for (var script :
80-
tree(scope)
81-
.filter(tokens(TokenTypes.METHOD_CALL))
82-
// Test for HTTP method name
83-
.filter(
84-
it ->
85-
tree(it)
86-
.filter(tokens(TokenTypes.IDENT))
87-
.anyMatch(e -> Router.METHODS.contains(e.getText().toUpperCase())))
88-
.toList()) {
89-
var scriptComment =
90-
children(script)
91-
.filter(tokens(TokenTypes.BLOCK_COMMENT_BEGIN))
92-
.findFirst()
93-
.orElse(null);
94-
if (scriptComment != null) {
95-
// ELIST -> EXPR -> STRING_LITERAL
96-
children(script)
97-
.filter(tokens(TokenTypes.ELIST))
98-
.findFirst()
99-
.flatMap(it -> children(it).filter(tokens(TokenTypes.EXPR)).findFirst())
100-
.flatMap(it -> children(it).filter(tokens(TokenTypes.STRING_LITERAL)).findFirst())
101-
.map(XpathUtil::getTextAttributeValue)
102-
.ifPresent(
103-
pattern -> {
104-
classDoc.addScript(pattern, new MethodDoc(this, script, scriptComment));
105-
});
106-
}
107-
}
79+
scripts(scope, null, null, new HashSet<>(), classDoc);
10880

10981
if (counter.get() > 0) {
11082
classes.put(classDoc.getName(), classDoc);
@@ -113,6 +85,76 @@ public Map<String, ClassDoc> traverse(DetailAST tree) {
11385
return classes;
11486
}
11587

88+
private void scripts(
89+
DetailAST scope, PathDoc pathDoc, String prefix, Set<DetailAST> visited, ClassDoc classDoc) {
90+
for (var script : tree(scope).filter(tokens(TokenTypes.METHOD_CALL)).toList()) {
91+
if (visited.add(script)) {
92+
// Test for HTTP method name
93+
var callName =
94+
tree(script)
95+
.filter(tokens(TokenTypes.IDENT))
96+
.findFirst()
97+
.map(DetailAST::getText)
98+
.stream()
99+
.findFirst()
100+
.orElseThrow(() -> new IllegalStateException("No method call found: " + script));
101+
var scriptComment =
102+
children(script)
103+
.filter(tokens(TokenTypes.BLOCK_COMMENT_BEGIN))
104+
.findFirst()
105+
.orElse(JavaDocNode.EMPTY_AST);
106+
if (Router.METHODS.contains(callName.toUpperCase())) {
107+
pathLiteral(script)
108+
.ifPresent(
109+
pattern -> {
110+
var scriptDoc =
111+
new ScriptDoc(
112+
this,
113+
callName.toUpperCase(),
114+
computePath(prefix, pattern),
115+
script,
116+
scriptComment);
117+
scriptDoc.setPath(pathDoc);
118+
classDoc.addScript(scriptDoc);
119+
});
120+
} else if ("path".equals(callName)) {
121+
pathLiteral(script)
122+
.ifPresent(
123+
path -> {
124+
scripts(
125+
script,
126+
new PathDoc(this, script, scriptComment),
127+
computePath(prefix, path),
128+
visited,
129+
classDoc);
130+
});
131+
}
132+
}
133+
}
134+
}
135+
136+
/**
137+
* ELIST -> EXPR -> STRING_LITERAL
138+
*
139+
* @param script Get string literal from method call.
140+
* @return String literal.
141+
*/
142+
private static Optional<String> pathLiteral(DetailAST script) {
143+
return children(script)
144+
.filter(tokens(TokenTypes.ELIST))
145+
.findFirst()
146+
.flatMap(it -> children(it).filter(tokens(TokenTypes.EXPR)).findFirst())
147+
.flatMap(it -> children(it).filter(tokens(TokenTypes.STRING_LITERAL)).findFirst())
148+
.map(XpathUtil::getTextAttributeValue);
149+
}
150+
151+
private String computePath(String prefix, String pattern) {
152+
if (prefix == null) {
153+
return Router.normalizePath(pattern);
154+
}
155+
return Router.noTrailingSlash(Router.normalizePath(prefix + pattern));
156+
}
157+
116158
private void traverse(
117159
DetailAST tree,
118160
Predicate<DetailAST> types,

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class JavaDocTag {
3434
CUSTOM_TAG.and(it -> it.getText().startsWith("@contact."));
3535
private static final Predicate<DetailNode> LICENSE =
3636
CUSTOM_TAG.and(it -> it.getText().startsWith("@license."));
37+
private static final Predicate<DetailNode> OPERATION_ID =
38+
CUSTOM_TAG.and(it -> it.getText().equals("@operationId"));
3739
private static final Predicate<DetailNode> EXTENSION =
3840
CUSTOM_TAG.and(it -> it.getText().startsWith("@x-"));
3941
private static final Predicate<DetailNode> THROWS =
@@ -250,4 +252,15 @@ public static void javaDocTag(
250252
}
251253
}
252254
}
255+
256+
public static String operationId(DetailNode javadoc) {
257+
var operationId = new ArrayList<String>();
258+
javaDocTag(
259+
javadoc,
260+
OPERATION_ID,
261+
(tag, value, text) -> {
262+
operationId.add(text);
263+
});
264+
return operationId.isEmpty() ? null : operationId.getFirst();
265+
}
253266
}

0 commit comments

Comments
 (0)