diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java index 40473debed..0f8455fdc1 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/AnnotationParser.java @@ -277,61 +277,16 @@ public static List parse(ParserContext ctx, String prefix, Type ty .parse(className) .ifPresent( doc -> { - operationExt.setPathDescription(doc.getDescription()); - operationExt.setPathSummary(doc.getSummary()); - doc.getTags().forEach(operationExt::addTag); - if (!doc.getExtensions().isEmpty()) { - operationExt.setPathExtensions(doc.getExtensions()); - } + JavaDocSetter.setPath(operationExt, doc); var parameterNames = - Optional.ofNullable(operationExt.getNode().parameters) - .orElse(List.of()) - .stream() - .map(p -> p.name) + operationExt.getNode().parameters.stream().map(p -> p.name).toList(); + var parameterTypes = + Stream.of(Type.getArgumentTypes(operationExt.getNode().desc)) + .map(Type::getClassName) .toList(); - doc.getMethod(operationExt.getOperationId(), parameterNames) + doc.getMethod(operationExt.getOperationId(), parameterTypes) .ifPresent( - methodDoc -> { - operationExt.setSummary(methodDoc.getSummary()); - operationExt.setDescription(methodDoc.getDescription()); - if (!methodDoc.getExtensions().isEmpty()) { - operationExt.setExtensions(methodDoc.getExtensions()); - } - methodDoc.getTags().forEach(operationExt::addTag); - // Parameters - for (var parameterName : parameterNames) { - var paramExt = - operationExt.getParameters().stream() - .filter(p -> p.getName().equals(parameterName)) - .findFirst() - .map(ParameterExt.class::cast) - .orElse(null); - var paramDoc = methodDoc.getParameterDoc(parameterName); - if (paramDoc != null) { - if (paramExt == null) { - operationExt.getRequestBody().setDescription(paramDoc); - } else { - paramExt.setDescription(paramDoc); - } - } - } - // return types - var defaultResponse = operationExt.getDefaultResponse(); - if (defaultResponse != null) { - defaultResponse.setDescription(methodDoc.getReturnDoc()); - } - for (var throwsDoc : methodDoc.getThrows().values()) { - var response = - operationExt.getResponse( - Integer.toString(throwsDoc.getStatusCode().value())); - if (response == null) { - response = - operationExt.addResponse( - Integer.toString(throwsDoc.getStatusCode().value())); - } - response.setDescription(throwsDoc.getText()); - } - }); + methodDoc -> JavaDocSetter.set(operationExt, methodDoc, parameterNames)); }); } catch (Exception x) { throw SneakyThrows.propagate(x); diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/JavaDocSetter.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/JavaDocSetter.java new file mode 100644 index 0000000000..3312601019 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/JavaDocSetter.java @@ -0,0 +1,85 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.jooby.internal.openapi.javadoc.JavaDocNode; +import io.jooby.internal.openapi.javadoc.MethodDoc; +import io.jooby.internal.openapi.javadoc.ScriptDoc; +import io.swagger.v3.oas.models.parameters.Parameter; + +public class JavaDocSetter { + + public static void setPath(OperationExt operation, JavaDocNode doc) { + operation.setPathDescription(doc.getDescription()); + operation.setPathSummary(doc.getSummary()); + doc.getTags().forEach(operation::addTag); + if (!doc.getExtensions().isEmpty()) { + operation.setPathExtensions(doc.getExtensions()); + } + } + + public static void set(OperationExt operation, ScriptDoc doc) { + var parameters = Optional.ofNullable(operation.getParameters()).orElse(List.of()); + var parameterNames = parameters.stream().map(Parameter::getName).collect(Collectors.toList()); + if (operation.getRequestBody() != null) { + var javaDocNames = new LinkedHashSet<>(doc.getJavadocParameterNames()); + parameterNames.forEach(javaDocNames::remove); + if (javaDocNames.size() == 1) { + // just add body name on lambda/script routes. + parameterNames.addAll(javaDocNames); + } + } + set(operation, doc, parameterNames); + } + + public static void set(OperationExt operation, MethodDoc doc, List parameterNames) { + operation.setOperationId( + Optional.ofNullable(operation.getOperationId()).orElse(doc.getOperationId())); + operation.setSummary(doc.getSummary()); + operation.setDescription(doc.getDescription()); + if (!doc.getExtensions().isEmpty()) { + operation.setExtensions(doc.getExtensions()); + } + doc.getTags().forEach(operation::addTag); + // Parameters + for (var parameterName : parameterNames) { + var paramExt = + operation.getParameters().stream() + .filter(p -> p.getName().equals(parameterName)) + .findFirst() + .map(ParameterExt.class::cast) + .orElse(null); + var paramDoc = doc.getParameterDoc(parameterName); + if (paramDoc != null) { + if (paramExt == null) { + var body = operation.getRequestBody(); + if (body != null) { + body.setDescription(paramDoc); + } + } else { + paramExt.setDescription(paramDoc); + } + } + } + // return types + var defaultResponse = operation.getDefaultResponse(); + if (defaultResponse != null) { + defaultResponse.setDescription(doc.getReturnDoc()); + } + for (var throwsDoc : doc.getThrows().values()) { + var response = operation.getResponse(throwsDoc.getCode()); + if (response == null) { + response = operation.addResponse(throwsDoc.getCode()); + } + response.setDescription(throwsDoc.getDescription()); + } + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java index e16c533e48..50426437dc 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/OperationExt.java @@ -211,6 +211,11 @@ public void setController(ClassNode controller) { this.controller = controller; } + @Override + public void setOperationId(String operationId) { + super.setOperationId(operationId); + } + @JsonIgnore public List getAllAnnotations() { return Stream.of( diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RequestBodyExt.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RequestBodyExt.java index 7171650660..4bc36f2b3d 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RequestBodyExt.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RequestBodyExt.java @@ -15,6 +15,10 @@ public class RequestBodyExt extends RequestBody { @JsonIgnore private String contentType = MediaType.JSON; + { + setRequired(true); + } + public String getJavaType() { return javaType; } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java index 9883c680c4..e086723705 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/RouteParser.java @@ -65,6 +65,9 @@ public List parse(ParserContext ctx, OpenAPIExt openapi) { Optional.ofNullable(ctx.getMainClass()).orElse(ctx.getRouter().getClassName()); ClassNode application = ctx.classNode(Type.getObjectType(applicationName.replace(".", "/"))); + // JavaDoc + addJavaDoc(ctx, ctx.getRouter().getClassName(), "", operations); + // swagger/openapi: for (OperationExt operation : operations) { operation.setApplication(application); @@ -100,6 +103,29 @@ public List parse(ParserContext ctx, OpenAPIExt openapi) { return result; } + private static void addJavaDoc( + ParserContext ctx, String className, String prefix, List operations) { + // javadoc + var offset = prefix == null || prefix.isEmpty() ? 0 : prefix.length(); + var javaDoc = ctx.javadoc().parse(className); + for (OperationExt operation : operations) { + // Script/lambda + if (operation.getController() == null) { + javaDoc + .flatMap( + doc -> + doc.getScript(operation.getMethod(), operation.getPattern().substring(offset))) + .ifPresent( + scriptDoc -> { + if (scriptDoc.getPath() != null) { + JavaDocSetter.setPath(operation, scriptDoc.getPath()); + } + JavaDocSetter.set(operation, scriptDoc); + }); + } + } + } + private void checkResponses(ParserContext ctx, OperationExt operation) { // checkResponse(ctx, operation, 200, operation.getDefaultResponse()); for (Map.Entry entry : operation.getResponses().entrySet()) { @@ -591,7 +617,9 @@ private List mountRouter( throw new UnsupportedOperationException(InsnSupport.toString(node)); } ClassNode classNode = ctx.classNode(router); - return parse(ctx.newContext(router), prefix, classNode); + var operations = parse(ctx.newContext(router), prefix, classNode); + addJavaDoc(ctx, router.getClassName(), prefix, operations); + return operations; } private List installApp( @@ -616,7 +644,9 @@ private List installApp( throw new UnsupportedOperationException(InsnSupport.toString(node)); } ClassNode classNode = ctx.classNode(router); - return parse(ctx.newContext(router), prefix, classNode); + var operations = parse(ctx.newContext(router), prefix, classNode); + addJavaDoc(ctx, router.getClassName(), prefix, operations); + return operations; } private Type kotlinSupplier(ParserContext ctx, MethodInsnNode node, AbstractInsnNode ins) { @@ -880,7 +910,8 @@ private OperationExt newRouteDescriptor( node.name.equals("apply") || node.name.equals("invoke") || node.name.startsWith("invoke$") - || node.name.contains("$lambda"); + || node.name.contains("$lambda") + || node.name.startsWith("fake$"); if (notSynthetic && !lambda) { operation.setOperationId(node.name); } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ClassDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ClassDoc.java index e3fbecfa7a..7380500ad0 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ClassDoc.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ClassDoc.java @@ -5,7 +5,8 @@ */ package io.jooby.internal.openapi.javadoc; -import static io.jooby.internal.openapi.javadoc.JavaDocSupport.*; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.*; +import static io.jooby.internal.openapi.javadoc.JavaDocSupport.getClassName; import java.util.*; import java.util.stream.Collectors; @@ -24,6 +25,7 @@ public class ClassDoc extends JavaDocNode { private final Map fields = new LinkedHashMap<>(); private final Map methods = new LinkedHashMap<>(); + private final Map scripts = new LinkedHashMap<>(); private final List servers; private final List contact; private final List license; @@ -115,7 +117,11 @@ private void defaultRecordMembers() { /* Virtual method */ var method = new MethodDoc( - context, createVirtualMember(name.getText(), TokenTypes.METHOD_DEF), memberDoc); + context, + createVirtualMember(name.getText(), TokenTypes.METHOD_DEF), + memberDoc) + .markAsVirtual(); + addMethod(method); }); } @@ -140,7 +146,7 @@ private void defaultEnumMembers() { } } - private static DetailAstImpl createVirtualMember(String name, int tokenType) { + private DetailAstImpl createVirtualMember(String name, int tokenType) { var publicMod = new DetailAstImpl(); publicMod.initialize( TokenTypes.LITERAL_PUBLIC, TokenUtil.getTokenName(TokenTypes.LITERAL_PUBLIC)); @@ -160,6 +166,10 @@ public void addMethod(MethodDoc method) { this.methods.put(toMethodSignature(method), method); } + public void addScript(ScriptDoc method) { + this.scripts.put(toScriptSignature(method), method); + } + public void addField(FieldDoc field) { this.fields.put(field.getName(), field); } @@ -168,48 +178,40 @@ public Optional getField(String name) { return Optional.ofNullable(fields.get(name)); } - public Optional getMethod(String name, List parameterNames) { - return Optional.ofNullable(methods.get(toMethodSignature(name, parameterNames))); + public Optional getMethod(String name, List types) { + return Optional.ofNullable(methods.get(toMethodSignature(name, types))); + } + + public Optional getScript(String method, String pattern) { + return Optional.ofNullable(scripts.get(toScriptSignature(method, pattern))); + } + + private String toScriptSignature(ScriptDoc method) { + return toScriptSignature(method.getMethod(), method.getPattern()); + } + + private String toScriptSignature(String method, String pattern) { + return method + "/" + pattern; } private String toMethodSignature(MethodDoc method) { - return toMethodSignature(method.getName(), method.getParameterNames()); + return toMethodSignature(method.getName(), method.getParameterTypes()); } - private String toMethodSignature(String methodName, List parameterNames) { - return methodName + parameterNames.stream().collect(Collectors.joining(", ", "(", ")")); + private String toMethodSignature(String methodName, List types) { + return methodName + types.stream().collect(Collectors.joining(", ", "(", ")")); } public String getSimpleName() { - return getSimpleName(node); + return JavaDocSupport.getSimpleName(node); } - protected String getSimpleName(DetailAST node) { - return node.findFirstToken(TokenTypes.IDENT).getText(); + public String getName() { + return getClassName(node); } - public String getName() { - var classScope = - Stream.concat( - Stream.of(node), - backward(node) - .filter( - tokens( - TokenTypes.CLASS_DEF, - TokenTypes.INTERFACE_DEF, - TokenTypes.ENUM_DEF, - TokenTypes.RECORD_DEF))) - .map(this::getSimpleName) - .toList(); - var packageScope = - backward(node) - .filter(tokens(TokenTypes.COMPILATION_UNIT)) - .findFirst() - .flatMap(it -> tree(it).filter(tokens(TokenTypes.PACKAGE_DEF)).findFirst()) - .map(it -> tree(it).filter(tokens(TokenTypes.IDENT)).map(DetailAST::getText).toList()) - .orElse(List.of()); - return Stream.concat(packageScope.stream(), classScope.stream()) - .collect(Collectors.joining(".")); + public String getPackage() { + return JavaDocSupport.getPackageName(node); } public boolean isRecord() { diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/FieldDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/FieldDoc.java index 6a8455ac17..13e1ba2117 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/FieldDoc.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/FieldDoc.java @@ -7,7 +7,6 @@ import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.DetailNode; -import com.puppycrawl.tools.checkstyle.api.TokenTypes; public class FieldDoc extends JavaDocNode { public FieldDoc(JavaDocParser ctx, DetailAST node, DetailAST javadoc) { @@ -19,6 +18,6 @@ public FieldDoc(JavaDocParser ctx, DetailAST node, DetailAST javadoc) { } public String getName() { - return node.findFirstToken(TokenTypes.IDENT).getText(); + return JavaDocSupport.getSimpleName(node); } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocNode.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocNode.java index ed1ec82139..9f21e33fe0 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocNode.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocNode.java @@ -5,8 +5,8 @@ */ package io.jooby.internal.openapi.javadoc; -import static io.jooby.internal.openapi.javadoc.JavaDocSupport.*; -import static io.jooby.internal.openapi.javadoc.JavaDocSupport.javadocToken; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.*; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.javadocToken; import java.util.*; import java.util.function.Predicate; @@ -47,6 +47,10 @@ static DetailNode toJavaDocNode(DetailAST node) { : new JavadocDetailNodeParser().parseJavadocAsDetailNode(node).getTree(); } + public DetailAST getNode() { + return node; + } + public Map getExtensions() { return extensions; } @@ -91,30 +95,34 @@ public String getDescription() { } public String getText() { - return getText(JavaDocSupport.forward(javadoc, JAVADOC_TAG).toList(), false); + return getText(JavaDocStream.forward(javadoc, JAVADOC_TAG).toList(), false); } protected static String getText(List nodes, boolean stripLeading) { var builder = new StringBuilder(); + var visited = new HashSet(); for (var node : nodes) { - if (node.getType() == JavadocTokenTypes.TEXT) { - var text = node.getText(); - if (stripLeading && Character.isWhitespace(text.charAt(0))) { - builder.append(' ').append(text.stripLeading()); - } else { - builder.append(text); - } - } else if (node.getType() == JavadocTokenTypes.NEWLINE) { - var next = JavadocUtil.getNextSibling(node); - if (next != null && next.getType() != JavadocTokenTypes.LEADING_ASTERISK) { - builder.append(next.getText()); + if (visited.add(node)) { + if (node.getType() == JavadocTokenTypes.TEXT) { + var text = node.getText(); + if (stripLeading && Character.isWhitespace(text.charAt(0))) { + builder.append(' ').append(text.stripLeading()); + } else { + builder.append(text); + } + } else if (node.getType() == JavadocTokenTypes.NEWLINE) { + var next = JavadocUtil.getNextSibling(node); + if (next != null && next.getType() != JavadocTokenTypes.LEADING_ASTERISK) { + builder.append(next.getText()); + visited.add(next); + } } } } - return builder.isEmpty() ? null : builder.toString().trim(); + return builder.isEmpty() ? null : builder.toString().trim().replaceAll("\\s+", " "); } - protected String toString(DetailNode node) { + protected static String toString(DetailNode node) { return DetailNodeTreeStringPrinter.printTree(node, "", ""); } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocParser.java index 1ef99d687e..b5a638ec00 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocParser.java @@ -7,8 +7,10 @@ import static com.puppycrawl.tools.checkstyle.JavaParser.parseFile; import static io.jooby.SneakyThrows.throwingFunction; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.*; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.tokens; import static io.jooby.internal.openapi.javadoc.JavaDocSupport.*; -import static io.jooby.internal.openapi.javadoc.JavaDocSupport.tokens; +import static java.util.Optional.ofNullable; import java.io.File; import java.nio.file.Files; @@ -22,8 +24,13 @@ import com.puppycrawl.tools.checkstyle.JavaParser; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.TokenTypes; +import com.puppycrawl.tools.checkstyle.utils.TokenUtil; +import com.puppycrawl.tools.checkstyle.utils.XpathUtil; +import io.jooby.Context; +import io.jooby.Router; public class JavaDocParser { + private record ScriptRef(String operationId, DetailAST comment) {} private final List baseDir; private final Map cache = new HashMap<>(); @@ -37,34 +44,34 @@ public JavaDocParser(List baseDir) { } public Optional parse(String typeName) { - return Optional.ofNullable(traverse(resolveType(typeName)).get(typeName)); + return ofNullable(traverse(resolveType(typeName)).get(typeName)); } - public Map traverse(DetailAST tree) { + private Map traverse(DetailAST tree) { var classes = new HashMap(); - var types = - tokens( - TokenTypes.ENUM_DEF, - TokenTypes.CLASS_DEF, - TokenTypes.INTERFACE_DEF, - TokenTypes.RECORD_DEF); traverse( - tree, - types, + List.of(tree), + TYPES, modifiers -> tree(modifiers).noneMatch(tokens(TokenTypes.LITERAL_PRIVATE)), (scope, comment) -> { + var scopes = typeHierarchy(scope); var counter = new AtomicInteger(0); counter.addAndGet(comment == JavaDocNode.EMPTY_AST ? 0 : 1); var classDoc = new ClassDoc(this, scope, comment); + // MVC routes traverse( - scope, + scopes, tokens(TokenTypes.VARIABLE_DEF, TokenTypes.METHOD_DEF), modifiers -> tree(modifiers).noneMatch(tokens(TokenTypes.LITERAL_STATIC)), (member, memberComment) -> { counter.addAndGet(memberComment == JavaDocNode.EMPTY_AST ? 0 : 1); // check member belong to current scope - if (scope == backward(member).filter(types).findFirst().orElse(null)) { + if (backward(member) + .filter(TYPES) + .findFirst() + .filter(scopes::contains) + .isPresent()) { if (member.getType() == TokenTypes.VARIABLE_DEF) { classDoc.addField(new FieldDoc(this, member, memberComment)); } else { @@ -72,10 +79,9 @@ public Map traverse(DetailAST tree) { } } }); + // Script routes + counter.addAndGet(scripts(scope, classDoc, null, null, new HashSet<>())); - if (classDoc.isRecord()) { - // complement with record parameter - } if (counter.get() > 0) { classes.put(classDoc.getName(), classDoc); } @@ -84,26 +90,221 @@ public Map traverse(DetailAST tree) { } private void traverse( - DetailAST tree, + List scopes, Predicate types, Predicate modifiers, BiConsumer action) { - for (var node : tree(tree).filter(types).toList()) { - var mods = - tree(node) - .filter(tokens(TokenTypes.MODIFIERS)) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Modifiers not found on " + node)); - if (modifiers.test(mods)) { - var docRoot = node.getType() == TokenTypes.VARIABLE_DEF ? mods.getParent() : mods; - var comment = - tree(docRoot) + + for (var scope : scopes) { + for (var node : tree(scope).filter(types).toList()) { + var mods = + tree(node) + .filter(tokens(TokenTypes.MODIFIERS)) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "Modifiers not found on " + TokenUtil.getTokenName(node.getType()))); + if (modifiers.test(mods)) { + var docRoot = node.getType() == TokenTypes.VARIABLE_DEF ? mods.getParent() : mods; + var comment = + tree(docRoot) + .filter(tokens(TokenTypes.BLOCK_COMMENT_BEGIN)) + .findFirst() + .orElse(JavaDocNode.EMPTY_AST); + action.accept(node, comment); + } + } + } + } + + private List typeHierarchy(DetailAST scope) { + var result = new ArrayList(); + // call parent members first, so we can inherit and override later. + children(scope) + .filter(tokens(TokenTypes.EXTENDS_CLAUSE)) + .findFirst() + .map(it -> JavaDocSupport.toQualifiedName(scope, getQualifiedName(it))) + .flatMap( + superClass -> + tree(resolveType(superClass)) + .filter(TYPES) + .filter(it -> superClass.endsWith("." + getSimpleName(it))) + .findFirst()) + .ifPresent(parent -> result.addAll(typeHierarchy(parent))); + result.add(scope); + return result; + } + + private int scripts( + DetailAST scope, ClassDoc classDoc, PathDoc pathDoc, String prefix, Set visited) { + var counter = new AtomicInteger(0); + for (var script : tree(scope).filter(tokens(TokenTypes.METHOD_CALL)).toList()) { + if (visited.add(script)) { + // Test for HTTP method name + var callName = + tree(script) + .filter(tokens(TokenTypes.IDENT)) + .findFirst() + .map(DetailAST::getText) + .stream() + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No method call found: " + script)); + var scriptComment = + children(script) .filter(tokens(TokenTypes.BLOCK_COMMENT_BEGIN)) .findFirst() .orElse(JavaDocNode.EMPTY_AST); - action.accept(node, comment); + if (scriptComment != JavaDocNode.EMPTY_AST) { + counter.incrementAndGet(); + } + if (Router.METHODS.contains(callName.toUpperCase())) { + pathLiteral(script) + .ifPresent( + pattern -> { + var resolvedComment = resolveScriptComment(classDoc, script, scriptComment); + if (scriptComment != resolvedComment.comment) { + counter.incrementAndGet(); + } + var scriptDoc = + new ScriptDoc( + this, + callName.toUpperCase(), + computePath(prefix, pattern), + script, + resolvedComment.comment); + if (resolvedComment.operationId() != null) { + scriptDoc.setOperationId(resolvedComment.operationId()); + } + scriptDoc.setPath(pathDoc); + classDoc.addScript(scriptDoc); + }); + } else if ("path".equals(callName)) { + pathLiteral(script) + .ifPresent( + path -> { + scripts( + script, + classDoc, + new PathDoc(this, script, scriptComment), + computePath(prefix, path), + visited); + }); + } } } + return counter.get(); + } + + /** + * get("/reference", this::findPetById); post("/static-reference", + * javadoc.input.LambdaRefApp::staticFindPetById); put("/external-reference", + * RequestHandler::external); get("/external-subPackage-reference", + * SubPackageHandler::subPackage); + * + * @param classDoc + * @param script + * @param defaultComment + * @return + */ + private ScriptRef resolveScriptComment( + ClassDoc classDoc, DetailAST script, DetailAST defaultComment) { + // ELIST -> LAMBDA (children) + // ELIST -> EXPR -> METHOD_REF (tree) + return children(script) + .filter(tokens(TokenTypes.ELIST)) + .findFirst() + .map( + statementList -> + children(statementList) + .filter(tokens(TokenTypes.LAMBDA)) + .findFirst() + .map(lambda -> new ScriptRef(null, defaultComment)) + .orElseGet( + () -> + tree(statementList) + .filter(tokens(TokenTypes.METHOD_REF)) + .findFirst() + .flatMap(ref -> ofNullable(resolveFromMethodRef(classDoc, ref))) + .orElseGet(() -> new ScriptRef(null, defaultComment)))) + .orElseGet(() -> new ScriptRef(null, defaultComment)); + } + + private ScriptRef resolveFromMethodRef(ClassDoc classDoc, DetailAST methodRef) { + var referenceOwner = getQualifiedName(methodRef); + DetailAST scope = null; + String className; + if (referenceOwner.equals("this")) { + scope = classDoc.getNode(); + className = classDoc.getName(); + } else { + // resolve className + className = toQualifiedName(classDoc.node, referenceOwner); + scope = resolveType(className); + if (scope == JavaDocNode.EMPTY_AST) { + // not found + return null; + } + } + var methodName = + children(methodRef).filter(tokens(TokenTypes.IDENT)).toList().getLast().getText(); + var method = + tree(scope) + .filter(tokens(TokenTypes.METHOD_DEF)) + .filter( + it -> + children(it) + .filter(tokens(TokenTypes.IDENT)) + .findFirst() + .filter(e -> e.getText().equals(methodName)) + .isPresent()) + // One Argument + .filter(it -> tree(it).filter(tokens(TokenTypes.PARAMETER_DEF)).count() == 1) + // Context Type + .filter( + it -> + tree(it) + .filter(tokens(TokenTypes.PARAMETER_DEF)) + .findFirst() + .flatMap(p -> children(p).filter(tokens(TokenTypes.TYPE)).findFirst()) + .filter( + type -> + toQualifiedName(classDoc.node, getQualifiedName(type)) + .equals(Context.class.getName())) + .isPresent()) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "No method found: " + className + "." + methodName)); + return children(method) + .filter(tokens(TokenTypes.MODIFIERS)) + .findFirst() + .flatMap(it -> children(it).filter(tokens(TokenTypes.BLOCK_COMMENT_BEGIN)).findFirst()) + .map(comment -> new ScriptRef(methodName, comment)) + .orElseGet(() -> new ScriptRef(null, JavaDocNode.EMPTY_AST)); + } + + /** + * ELIST -> EXPR -> STRING_LITERAL + * + * @param script Get string literal from method call. + * @return String literal. + */ + private static Optional pathLiteral(DetailAST script) { + return children(script) + .filter(tokens(TokenTypes.ELIST)) + .findFirst() + .flatMap(it -> children(it).filter(tokens(TokenTypes.EXPR)).findFirst()) + .flatMap(it -> children(it).filter(tokens(TokenTypes.STRING_LITERAL)).findFirst()) + .map(XpathUtil::getTextAttributeValue); + } + + private String computePath(String prefix, String pattern) { + if (prefix == null) { + return Router.normalizePath(pattern); + } + return Router.noTrailingSlash(Router.normalizePath(prefix + pattern)); } public DetailAST resolve(Path path) { diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocStream.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocStream.java new file mode 100644 index 0000000000..b8c86e5bdd --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocStream.java @@ -0,0 +1,248 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.javadoc; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import com.puppycrawl.tools.checkstyle.api.DetailAST; +import com.puppycrawl.tools.checkstyle.api.DetailNode; +import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; + +public final class JavaDocStream { + + public static Predicate tokens(Integer... types) { + return tokens(Set.of(types)); + } + + private static Predicate tokens(Set types) { + return it -> types.contains(it.getType()); + } + + public static Predicate javadocToken(Integer... types) { + return javadocToken(Set.of(types)); + } + + private static Predicate javadocToken(Set types) { + return it -> types.contains(it.getType()); + } + + /** + * Traverse the tree from current node to parent (backward). + * + * @param node Starting point + * @return Stream. + */ + public static Stream backward(DetailAST node) { + return backward(ASTNode.ast(node)); + } + + /** + * Traverse the tree from the current node to children and sibling (forward). + * + * @param node Starting point + * @return Stream. + */ + public static Stream forward(DetailAST node) { + return forward(ASTNode.ast(node)); + } + + /** + * Traverse the tree from the current node to children and sibling (forward) but keeping the scope + * to the given node (root). + * + * @param node Root node. + * @return Stream. + */ + public static Stream tree(DetailAST node) { + return tree(ASTNode.ast(node)); + } + + public static Stream tree(DetailNode node) { + return tree(ASTNode.javadoc(node)); + } + + public static Stream children(DetailAST node) { + return stream(childrenIterator(ASTNode.ast(node))); + } + + public static Stream children(DetailNode node) { + return stream(childrenIterator(ASTNode.javadoc(node))); + } + + public static Stream backward(DetailNode node) { + return backward(ASTNode.javadoc(node)); + } + + public static Stream forward(DetailNode node, Predicate stopOn) { + var nodes = forward(ASTNode.javadoc(node)).toList(); + var result = new ArrayList(); + for (var it : nodes) { + if (stopOn.test(it)) { + break; + } + result.add(it); + } + return result.stream(); + } + + private static Stream backward(ASTNode node) { + return stream(backwardIterator(node)); + } + + private static Stream forward(ASTNode node) { + return stream(forwardIterator(node)); + } + + private static Stream tree(ASTNode node) { + return stream(treeIterator(node)); + } + + private static Stream stream(Iterator iterator) { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); + } + + private static Iterator treeIterator(ASTNode node) { + return forwardIterator(node, false); + } + + private static Iterator forwardIterator(ASTNode node) { + return forwardIterator(node, true); + } + + private static Iterator childrenIterator(ASTNode node) { + return new Iterator<>() { + Function, ASTNode> direction = null; + ASTNode it = node; + + @Override + public boolean hasNext() { + if (direction == null) { + direction = ASTNode::getFirstChild; + } else { + direction = ASTNode::getNextSibling; + } + return direction.apply(it) != null; + } + + @Override + public T next() { + it = direction.apply(it); + return it.getNode(); + } + }; + } + + private static Iterator backwardIterator(ASTNode node) { + return new Iterator<>() { + ASTNode it = node; + + @Override + public boolean hasNext() { + return it.getParent() != null; + } + + @Override + public T next() { + it = it.getParent(); + return it.getNode(); + } + }; + } + + private static Iterator forwardIterator(ASTNode node, boolean full) { + return new Iterator<>() { + ASTNode it = node; + final Stack> stack = new Stack<>(); + + @Override + public boolean hasNext() { + return it != null; + } + + @Override + public T next() { + if (it.getNextSibling() != null) { + if (full || it != node) { + stack.push(it.getNextSibling()); + } + } + var current = it; + var child = it.getFirstChild(); + if (child == null) { + if (!stack.isEmpty()) { + it = stack.pop(); + } else { + it = null; + } + } else { + it = child; + } + return current.getNode(); + } + }; + } + + private interface ASTNode { + ASTNode getFirstChild(); + + ASTNode getNextSibling(); + + ASTNode getParent(); + + Node getNode(); + + static ASTNode ast(DetailAST node) { + return ast(node, DetailAST::getParent, DetailAST::getFirstChild, DetailAST::getNextSibling); + } + + static ASTNode javadoc(DetailNode node) { + return ast( + node, DetailNode::getParent, JavadocUtil::getFirstChild, JavadocUtil::getNextSibling); + } + + static ASTNode ast( + N node, Function parent, Function child, Function sibling) { + return new ASTNode<>() { + @Override + public ASTNode getParent() { + var parentNode = parent.apply(node); + if (parentNode == null) { + return null; + } + return ast(parentNode, parent, child, sibling); + } + + @Override + public ASTNode getFirstChild() { + var childNode = child.apply(node); + if (childNode == null) { + return null; + } + return ast(childNode, parent, child, sibling); + } + + @Override + public N getNode() { + return node; + } + + @Override + public ASTNode getNextSibling() { + var siblingNode = sibling.apply(node); + if (siblingNode == null) { + return null; + } + return ast(siblingNode, parent, child, sibling); + } + }; + } + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocSupport.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocSupport.java index 6000c4ea52..59a5c536ce 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocSupport.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocSupport.java @@ -5,248 +5,119 @@ */ package io.jooby.internal.openapi.javadoc; -import java.util.*; -import java.util.function.Function; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.*; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.tokens; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.tree; + +import java.util.List; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import com.puppycrawl.tools.checkstyle.api.DetailAST; -import com.puppycrawl.tools.checkstyle.api.DetailNode; -import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; - -public final class JavaDocSupport { +import com.puppycrawl.tools.checkstyle.api.TokenTypes; +import com.puppycrawl.tools.checkstyle.utils.TokenUtil; - public static Predicate tokens(Integer... types) { - return tokens(Set.of(types)); - } +public class JavaDocSupport { - private static Predicate tokens(Set types) { - return it -> types.contains(it.getType()); - } + public static final Predicate TYPES = + tokens( + TokenTypes.CLASS_DEF, + TokenTypes.INTERFACE_DEF, + TokenTypes.ENUM_DEF, + TokenTypes.RECORD_DEF); - public static Predicate javadocToken(Integer... types) { - return javadocToken(Set.of(types)); - } - - private static Predicate javadocToken(Set types) { - return it -> types.contains(it.getType()); - } + public static final Predicate EXTENDED_TYPES = + TYPES.or(tokens(TokenTypes.VARIABLE_DEF, TokenTypes.PARAMETER_DEF)); /** - * Traverse the tree from current node to parent (backward). + * Name from class, method, field, parameter. * - * @param node Starting point - * @return Stream. + * @param node Node. + * @return Name. */ - public static Stream backward(DetailAST node) { - return backward(ASTNode.ast(node)); - } - - /** - * Traverse the tree from the current node to children and sibling (forward). - * - * @param node Starting point - * @return Stream. - */ - public static Stream forward(DetailAST node) { - return forward(ASTNode.ast(node)); - } - - /** - * Traverse the tree from the current node to children and sibling (forward) but keeping the scope - * to the given node (root). - * - * @param node Root node. - * @return Stream. - */ - public static Stream tree(DetailAST node) { - return tree(ASTNode.ast(node)); - } - - public static Stream tree(DetailNode node) { - return tree(ASTNode.javadoc(node)); - } - - public static Stream children(DetailAST node) { - return stream(childrenIterator(ASTNode.ast(node))); - } - - public static Stream children(DetailNode node) { - return stream(childrenIterator(ASTNode.javadoc(node))); - } - - public static Stream backward(DetailNode node) { - return backward(ASTNode.javadoc(node)); - } - - public static Stream forward(DetailNode node) { - return forward(ASTNode.javadoc(node)); - } - - public static Stream forward(DetailNode node, Predicate stopOn) { - var nodes = forward(ASTNode.javadoc(node)).toList(); - var result = new ArrayList(); - for (var it : nodes) { - if (stopOn.test(it)) { - break; - } - result.add(it); - } - return result.stream(); - } - - private static Stream backward(ASTNode node) { - return stream(backwardIterator(node)); - } - - private static Stream forward(ASTNode node) { - return stream(forwardIterator(node)); - } - - private static Stream tree(ASTNode node) { - return stream(treeIterator(node)); - } - - private static Stream stream(Iterator iterator) { - return StreamSupport.stream( - Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); - } - - private static Iterator treeIterator(ASTNode node) { - return forwardIterator(node, false); - } - - private static Iterator forwardIterator(ASTNode node) { - return forwardIterator(node, true); - } - - private static Iterator childrenIterator(ASTNode node) { - return new Iterator<>() { - Function, ASTNode> direction = null; - ASTNode it = node; - - @Override - public boolean hasNext() { - if (direction == null) { - direction = ASTNode::getFirstChild; - } else { - direction = ASTNode::getNextSibling; - } - return direction.apply(it) != null; - } - - @Override - public T next() { - it = direction.apply(it); - return it.getNode(); - } - }; - } - - private static Iterator backwardIterator(ASTNode node) { - return new Iterator<>() { - ASTNode it = node; - - @Override - public boolean hasNext() { - return it.getParent() != null; - } - - @Override - public T next() { - it = it.getParent(); - return it.getNode(); - } - }; - } - - private static Iterator forwardIterator(ASTNode node, boolean full) { - return new Iterator<>() { - ASTNode it = node; - final Stack> stack = new Stack<>(); - - @Override - public boolean hasNext() { - return it != null; - } - - @Override - public T next() { - if (it.getNextSibling() != null) { - if (full || it != node) { - stack.push(it.getNextSibling()); + public static String getSimpleName(DetailAST node) { + checkTypeDef(EXTENDED_TYPES, node); + return node.findFirstToken(TokenTypes.IDENT).getText(); + } + + public static String getClassName(DetailAST node) { + checkTypeDef(TYPES, node); + var classScope = + Stream.concat(Stream.of(node), backward(node).filter(TYPES)) + .map(JavaDocSupport::getSimpleName) + .toList(); + + return Stream.concat(Stream.of(getPackageName(node)), classScope.stream()) + .collect(Collectors.joining(".")); + } + + public static DetailAST getCompilationUnit(DetailAST node) { + return backward(node) + .filter(tokens(TokenTypes.COMPILATION_UNIT)) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "Compilation unit missing: " + TokenUtil.getTokenName(node.getType()))); + } + + public static String getPackageName(DetailAST node) { + return String.join( + ".", + tree(getCompilationUnit(node)) + .filter(tokens(TokenTypes.PACKAGE_DEF)) + .findFirst() + .map(it -> tree(it).filter(tokens(TokenTypes.IDENT)).map(DetailAST::getText).toList()) + .orElse(List.of())); + } + + public static String getQualifiedName(DetailAST node) { + return tree(node.getFirstChild()) + .filter(tokens(TokenTypes.DOT).negate()) + .map(DetailAST::getText) + .collect(Collectors.joining(".")); + } + + public static String toQualifiedName(DetailAST classDef, String typeName) { + return switch (typeName) { + case "char", "boolean", "int", "short", "long", "float", "double" -> typeName; + case "Character" -> Character.class.getName(); + case "Boolean" -> Boolean.class.getName(); + case "Integer" -> Integer.class.getName(); + case "Short" -> Short.class.getName(); + case "Long" -> Long.class.getName(); + case "Float" -> Float.class.getName(); + case "Double" -> Double.class.getName(); + case "String" -> String.class.getName(); + default -> { + checkTypeDef(TYPES, classDef); + if (!typeName.contains(".")) { + if (!getSimpleName(classDef).equals(typeName)) { + var cu = getCompilationUnit(classDef); + yield children(cu) + .filter(tokens(TokenTypes.IMPORT)) + .map( + it -> + tree(it.getFirstChild()) + .filter(tokens(TokenTypes.DOT).negate()) + .map(DetailAST::getText) + .collect(Collectors.joining("."))) + .filter(qualifiedName -> qualifiedName.endsWith("." + typeName)) + .findFirst() + .orElseGet(() -> String.join(".", getPackageName(classDef), typeName)); } } - var current = it; - var child = it.getFirstChild(); - if (child == null) { - if (!stack.isEmpty()) { - it = stack.pop(); - } else { - it = null; - } - } else { - it = child; - } - return current.getNode(); + // Already qualified. + yield typeName; } }; } - private interface ASTNode { - ASTNode getFirstChild(); - - ASTNode getNextSibling(); - - ASTNode getParent(); - - Node getNode(); - - static ASTNode ast(DetailAST node) { - return ast(node, DetailAST::getParent, DetailAST::getFirstChild, DetailAST::getNextSibling); - } - - static ASTNode javadoc(DetailNode node) { - return ast( - node, DetailNode::getParent, JavadocUtil::getFirstChild, JavadocUtil::getNextSibling); - } - - static ASTNode ast( - N node, Function parent, Function child, Function sibling) { - return new ASTNode<>() { - @Override - public ASTNode getParent() { - var parentNode = parent.apply(node); - if (parentNode == null) { - return null; - } - return ast(parentNode, parent, child, sibling); - } - - @Override - public ASTNode getFirstChild() { - var childNode = child.apply(node); - if (childNode == null) { - return null; - } - return ast(childNode, parent, child, sibling); - } - - @Override - public N getNode() { - return node; - } - - @Override - public ASTNode getNextSibling() { - var siblingNode = sibling.apply(node); - if (siblingNode == null) { - return null; - } - return ast(siblingNode, parent, child, sibling); - } - }; + private static void checkTypeDef(Predicate predicate, DetailAST node) { + if (!predicate.test(node)) { + throw new IllegalArgumentException( + "Must be a type definition, found: " + TokenUtil.getTokenName(node.getType())); } } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocTag.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocTag.java index cf650c38af..d0a7b53f86 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocTag.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/JavaDocTag.java @@ -6,8 +6,8 @@ package io.jooby.internal.openapi.javadoc; import static io.jooby.internal.openapi.javadoc.JavaDocNode.getText; -import static io.jooby.internal.openapi.javadoc.JavaDocSupport.*; -import static io.jooby.internal.openapi.javadoc.JavaDocSupport.children; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.*; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.children; import java.util.*; import java.util.function.Function; @@ -18,6 +18,7 @@ import io.jooby.SneakyThrows.Consumer2; import io.jooby.SneakyThrows.Consumer3; import io.jooby.StatusCode; +import io.jooby.internal.openapi.ResponseExt; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.servers.Server; @@ -34,6 +35,8 @@ public class JavaDocTag { CUSTOM_TAG.and(it -> it.getText().startsWith("@contact.")); private static final Predicate LICENSE = CUSTOM_TAG.and(it -> it.getText().startsWith("@license.")); + private static final Predicate OPERATION_ID = + CUSTOM_TAG.and(it -> it.getText().equals("@operationId")); private static final Predicate EXTENSION = CUSTOM_TAG.and(it -> it.getText().startsWith("@x-")); private static final Predicate THROWS = @@ -91,11 +94,12 @@ private static List openApiComponent( }); var result = new ArrayList(); if (!values.isEmpty()) { - var output = MiniYamlDocParser.parse(values); + var output = TinyYamlDocParser.parse(values); var itemList = output.get(path); if (!(itemList instanceof List)) { itemList = List.of(itemList); } + //noinspection unchecked,rawtypes ((List) itemList) .forEach( it -> { @@ -107,8 +111,8 @@ private static List openApiComponent( return result; } - public static Map throwList(DetailNode node) { - var result = new LinkedHashMap(); + public static Map throwList(DetailNode node) { + var result = new LinkedHashMap(); javaDocTag( node, THROWS, @@ -149,7 +153,13 @@ public static Map throwList(DetailNode node) { .findFirst()) .orElse(null); if (statusCode != null) { - var throwsDoc = new ThrowsDoc(statusCode, text); + if (text == null) { + text = statusCode.reason(); + } else { + text = statusCode.reason() + ": " + text; + } + var throwsDoc = new ResponseExt(Integer.toString(statusCode.value())); + throwsDoc.setDescription(text); result.putIfAbsent(statusCode, throwsDoc); } }); @@ -166,7 +176,7 @@ public static Map extensions(DetailNode node) { values.add(tag.getText().substring(1)); values.add(value); }); - return MiniYamlDocParser.parse(values); + return TinyYamlDocParser.parse(values); } public static List tags(DetailNode node) { @@ -202,7 +212,7 @@ public static List tags(DetailNode node) { } }); if (!values.isEmpty()) { - var tagMap = MiniYamlDocParser.parse(values); + var tagMap = TinyYamlDocParser.parse(values); var tags = tagMap.get("tag"); if (!(tags instanceof List)) { tags = List.of(tags); @@ -250,4 +260,15 @@ public static void javaDocTag( } } } + + public static String operationId(DetailNode javadoc) { + var operationId = new ArrayList(); + javaDocTag( + javadoc, + OPERATION_ID, + (tag, value, text) -> { + operationId.add(text); + }); + return operationId.isEmpty() ? null : operationId.getFirst(); + } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/MethodDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/MethodDoc.java index 620523f2f8..9842a1c8c3 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/MethodDoc.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/MethodDoc.java @@ -5,7 +5,7 @@ */ package io.jooby.internal.openapi.javadoc; -import static io.jooby.internal.openapi.javadoc.JavaDocSupport.*; +import static io.jooby.internal.openapi.javadoc.JavaDocStream.*; import java.util.*; import java.util.stream.Stream; @@ -15,14 +15,17 @@ import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import io.jooby.StatusCode; +import io.jooby.internal.openapi.ResponseExt; public class MethodDoc extends JavaDocNode { - - private Map throwList; + private String operationId; + private Map throwList; + private List parameterTypes = null; public MethodDoc(JavaDocParser ctx, DetailAST node, DetailAST javadoc) { super(ctx, node, javadoc); throwList = JavaDocTag.throwList(this.javadoc); + operationId = JavaDocTag.operationId(this.javadoc); } MethodDoc(JavaDocParser ctx, DetailAST node, DetailNode javadoc) { @@ -33,20 +36,54 @@ public String getName() { return node.findFirstToken(TokenTypes.IDENT).getText(); } - public List getParameterNames() { - var result = new ArrayList(); - var index = 0; - for (var parameterDef : tree(node).filter(tokens(TokenTypes.PARAMETER_DEF)).toList()) { - var name = - children(parameterDef) - .filter(tokens(TokenTypes.IDENT)) + public String getOperationId() { + return operationId; + } + + public void setOperationId(String operationId) { + this.operationId = operationId; + } + + public List getParameterTypes() { + if (parameterTypes == null) { + parameterTypes = new ArrayList<>(); + var classDef = + backward(node) + .filter(JavaDocSupport.TYPES) .findFirst() - .map(DetailAST::getText) - .orElse("param" + index); - result.add(name); - index++; + .orElseThrow(() -> new IllegalArgumentException("Class not found: " + node)); + for (var parameter : tree(node).filter(tokens(TokenTypes.PARAMETER_DEF)).toList()) { + var type = + children(parameter) + .filter(tokens(TokenTypes.TYPE)) + .findFirst() + .orElseThrow( + () -> + new IllegalArgumentException( + "Parameter type not found: " + + JavaDocSupport.getSimpleName(parameter))); + parameterTypes.add( + JavaDocSupport.toQualifiedName(classDef, JavaDocSupport.getQualifiedName(type))); + } } - return result; + return parameterTypes; + } + + public MethodDoc markAsVirtual() { + parameterTypes = List.of(); + return this; + } + + public List getJavadocParameterNames() { + return tree(javadoc) + // must be a tag + .filter(javadocToken(JavadocTokenTypes.JAVADOC_TAG)) + .flatMap( + it -> + children(it) + .filter(javadocToken(JavadocTokenTypes.PARAMETER_NAME)) + .map(DetailNode::getText)) + .toList(); } public String getParameterDoc(String name) { @@ -69,7 +106,7 @@ public String getParameterDoc(String name) { getText( Stream.of(it.getChildren()) .filter(e -> e.getType() == JavadocTokenTypes.DESCRIPTION) - .flatMap(JavaDocSupport::tree) + .flatMap(JavaDocStream::tree) .toList(), true)) .filter(it -> !it.isEmpty()) @@ -89,7 +126,7 @@ public String getReturnDoc() { .orElse(null); } - public Map getThrows() { + public Map getThrows() { return throwList; } } diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/PathDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/PathDoc.java new file mode 100644 index 0000000000..8757c7f783 --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/PathDoc.java @@ -0,0 +1,14 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.javadoc; + +import com.puppycrawl.tools.checkstyle.api.DetailAST; + +public class PathDoc extends JavaDocNode { + public PathDoc(JavaDocParser ctx, DetailAST node, DetailAST comment) { + super(ctx, node, comment); + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ScriptDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ScriptDoc.java new file mode 100644 index 0000000000..4cb6626f7b --- /dev/null +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ScriptDoc.java @@ -0,0 +1,42 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.internal.openapi.javadoc; + +import com.puppycrawl.tools.checkstyle.api.DetailAST; + +public class ScriptDoc extends MethodDoc { + private final String method; + private final String pattern; + private PathDoc path; + + public ScriptDoc( + JavaDocParser ctx, String method, String pattern, DetailAST node, DetailAST javadoc) { + super(ctx, node, javadoc); + this.method = method; + this.pattern = pattern; + } + + public PathDoc getPath() { + return path; + } + + public void setPath(PathDoc path) { + this.path = path; + } + + public String getMethod() { + return method; + } + + public String getPattern() { + return pattern; + } + + @Override + public String toString() { + return method + " " + pattern; + } +} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ThrowsDoc.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ThrowsDoc.java deleted file mode 100644 index 0fe1520e38..0000000000 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/ThrowsDoc.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Jooby https://jooby.io - * Apache License Version 2.0 https://jooby.io/LICENSE.txt - * Copyright 2014 Edgar Espina - */ -package io.jooby.internal.openapi.javadoc; - -import io.jooby.StatusCode; - -public class ThrowsDoc { - private final String text; - private final StatusCode statusCode; - - public ThrowsDoc(StatusCode statusCode, String text) { - this.statusCode = statusCode; - if (text == null) { - this.text = statusCode.reason(); - } else { - this.text = statusCode.reason() + ": " + text; - } - } - - public String getText() { - return text; - } - - public StatusCode getStatusCode() { - return statusCode; - } -} diff --git a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/MiniYamlDocParser.java b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/TinyYamlDocParser.java similarity index 97% rename from modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/MiniYamlDocParser.java rename to modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/TinyYamlDocParser.java index cb41bd937d..65acab89f5 100644 --- a/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/MiniYamlDocParser.java +++ b/modules/jooby-openapi/src/main/java/io/jooby/internal/openapi/javadoc/TinyYamlDocParser.java @@ -10,7 +10,7 @@ import java.util.List; import java.util.Map; -public class MiniYamlDocParser { +public class TinyYamlDocParser { @SuppressWarnings("unchecked") public static Map parse(List properties) { // The root of our final tree structure. @@ -102,7 +102,7 @@ private static Object restructureNode(Object node) { listOfObjects.add(objectMap); } if (listOfObjects.size() == 1 - && restructuredMap.keySet().stream().noneMatch(MiniYamlDocParser::startsWithDash)) { + && restructuredMap.keySet().stream().noneMatch(TinyYamlDocParser::startsWithDash)) { return listOfObjects.getFirst(); } return listOfObjects; diff --git a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIYamlTest.java b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIYamlTest.java index e77bda570a..223566efda 100644 --- a/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIYamlTest.java +++ b/modules/jooby-openapi/src/test/java/io/jooby/openapi/OpenAPIYamlTest.java @@ -62,6 +62,7 @@ public void shouldGenerateMinApp(OpenAPIResult result) { + " application/json:\n" + " schema:\n" + " $ref: \"#/components/schemas/Pet\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -77,6 +78,7 @@ public void shouldGenerateMinApp(OpenAPIResult result) { + " application/json:\n" + " schema:\n" + " $ref: \"#/components/schemas/Pet\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -91,6 +93,7 @@ public void shouldGenerateMinApp(OpenAPIResult result) { + " application/json:\n" + " schema:\n" + " $ref: \"#/components/schemas/Pet\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -141,6 +144,7 @@ public void shouldGenerateMinApp(OpenAPIResult result) { + " application/x-www-form-urlencoded:\n" + " schema:\n" + " $ref: \"#/components/schemas/Pet\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -223,6 +227,7 @@ public void shouldGenerateKtMinApp(OpenAPIResult result) { + " application/json:\n" + " schema:\n" + " $ref: \"#/components/schemas/Pet\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -238,6 +243,7 @@ public void shouldGenerateKtMinApp(OpenAPIResult result) { + " application/json:\n" + " schema:\n" + " $ref: \"#/components/schemas/Pet\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -252,6 +258,7 @@ public void shouldGenerateKtMinApp(OpenAPIResult result) { + " application/json:\n" + " schema:\n" + " $ref: \"#/components/schemas/Pet\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -302,6 +309,7 @@ public void shouldGenerateKtMinApp(OpenAPIResult result) { + " application/x-www-form-urlencoded:\n" + " schema:\n" + " $ref: \"#/components/schemas/Pet\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -358,6 +366,7 @@ public void shouldDoForm(OpenAPIResult result) { + " properties:\n" + " name:\n" + " type: string\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -385,6 +394,7 @@ public void shouldDoForm(OpenAPIResult result) { + " picture:\n" + " type: string\n" + " format: binary\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -400,6 +410,7 @@ public void shouldDoForm(OpenAPIResult result) { + " application/x-www-form-urlencoded:\n" + " schema:\n" + " $ref: \"#/components/schemas/AForm\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -440,6 +451,7 @@ public void shouldDoMvcForm(OpenAPIResult result) { + " properties:\n" + " name:\n" + " type: string\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -463,6 +475,7 @@ public void shouldDoMvcForm(OpenAPIResult result) { + " picture:\n" + " type: string\n" + " format: binary\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" @@ -478,6 +491,7 @@ public void shouldDoMvcForm(OpenAPIResult result) { + " multipart/form-data:\n" + " schema:\n" + " $ref: \"#/components/schemas/AForm\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" diff --git a/modules/jooby-openapi/src/test/java/issues/Issue1592.java b/modules/jooby-openapi/src/test/java/issues/Issue1592.java index 27374967eb..1fa1e6b9cf 100644 --- a/modules/jooby-openapi/src/test/java/issues/Issue1592.java +++ b/modules/jooby-openapi/src/test/java/issues/Issue1592.java @@ -30,6 +30,7 @@ public void shouldParseNestedTypes(OpenAPIResult result) { + " application/json:\n" + " schema:\n" + " $ref: \"#/components/schemas/FairData\"\n" + + " required: true\n" + " responses:\n" + " \"200\":\n" + " description: Success\n" diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java index 813648aff4..6162055be6 100644 --- a/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/ApiDocTest.java @@ -13,7 +13,16 @@ public class ApiDocTest { @OpenAPITest(value = AppLibrary.class) - public void shouldGenerateDoc(OpenAPIResult result) { + public void shouldGenerateMvcDoc(OpenAPIResult result) { + checkResult(result); + } + + @OpenAPITest(value = ScriptLibrary.class) + public void shouldGenerateScriptDoc(OpenAPIResult result) { + checkResult(result); + } + + private void checkResult(OpenAPIResult result) { assertEquals( "openapi: 3.0.1\n" + "info:\n" diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/InstalledRouter.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/InstalledRouter.java new file mode 100644 index 0000000000..c12507a0f4 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/InstalledRouter.java @@ -0,0 +1,23 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +import io.jooby.Context; +import io.jooby.Jooby; + +public class InstalledRouter extends Jooby { + + { + /* + * Installed operation. + * + * @operationId installedOp + */ + get("/installed", Context::getRequestPath); + + mount(new MountedRouter()); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/MountedApp.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/MountedApp.java new file mode 100644 index 0000000000..7c334555ac --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/MountedApp.java @@ -0,0 +1,26 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +import io.jooby.Context; +import io.jooby.Jooby; + +public class MountedApp extends Jooby { + { + /* + * This is the main router. + */ + get("/main", Context::getRequestPath); + + mount(new MountedRouter()); + + mount("/mount-point", new MountedRouter()); + + install(InstalledRouter::new); + + install("/install-point", InstalledRouter::new); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/MountedDocTest.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/MountedDocTest.java new file mode 100644 index 0000000000..8a88f6e875 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/MountedDocTest.java @@ -0,0 +1,92 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.jooby.openapi.OpenAPIResult; +import io.jooby.openapi.OpenAPITest; + +public class MountedDocTest { + + @OpenAPITest(value = MountedApp.class) + public void shouldGenerateDocFromMountedApp(OpenAPIResult result) { + assertEquals( + "openapi: 3.0.1\n" + + "info:\n" + + " title: Mounted API\n" + + " description: Mounted API description\n" + + " version: \"1.0\"\n" + + "paths:\n" + + " /main:\n" + + " get:\n" + + " summary: This is the main router.\n" + + " operationId: getMain\n" + + " responses:\n" + + " \"200\":\n" + + " description: Success\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: string\n" + + " /mounted:\n" + + " get:\n" + + " summary: Mounted operation.\n" + + " operationId: mountedOp3\n" + + " responses:\n" + + " \"200\":\n" + + " description: Success\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: string\n" + + " /mount-point/mounted:\n" + + " get:\n" + + " summary: Mounted operation.\n" + + " operationId: mountedOp2\n" + + " responses:\n" + + " \"200\":\n" + + " description: Success\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: string\n" + + " /installed:\n" + + " get:\n" + + " summary: Installed operation.\n" + + " operationId: installedOp\n" + + " responses:\n" + + " \"200\":\n" + + " description: Success\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: string\n" + + " /install-point/installed:\n" + + " get:\n" + + " summary: Installed operation.\n" + + " operationId: installedOp2\n" + + " responses:\n" + + " \"200\":\n" + + " description: Success\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: string\n" + + " /install-point/mounted:\n" + + " get:\n" + + " summary: Mounted operation.\n" + + " operationId: mountedOp4\n" + + " responses:\n" + + " \"200\":\n" + + " description: Success\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: string\n", + result.toYaml()); + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/MountedRouter.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/MountedRouter.java new file mode 100644 index 0000000000..22bd399dc8 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/MountedRouter.java @@ -0,0 +1,23 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +import io.jooby.Context; +import io.jooby.Jooby; + +public class MountedRouter extends Jooby { + + { + get("/mounted", this::mountedOp); + } + + /* + * Mounted operation. + */ + private String mountedOp(Context context) { + return ""; + } +} diff --git a/modules/jooby-openapi/src/test/java/issues/i3729/api/ScriptLibrary.java b/modules/jooby-openapi/src/test/java/issues/i3729/api/ScriptLibrary.java new file mode 100644 index 0000000000..6a5368a9f0 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/issues/i3729/api/ScriptLibrary.java @@ -0,0 +1,112 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package issues.i3729.api; + +import java.util.ArrayList; +import java.util.List; + +import io.jooby.Context; +import io.jooby.Jooby; + +/** + * Library API. + * + *

Available data: Books and authors. + * + * @version 4.0.0 + * @server.url https://api.fake-museum-example.com/v1 + * @contact.name Jooby + * @contact.url https://jooby.io + * @license.name Apache + * @contact.email support@jooby.io + * @license.url https://jooby.io/LICENSE + * @x-logo.url https://redocly.github.io/redoc/museum-logo.png + * @x-logo.altText Museum logo + */ +public class ScriptLibrary extends Jooby { + + { + /* + * Library API. + * + *

Contains all operations for creating, updating and fetching books. + * + * @tag.name Library + * @tag.description Access to all books. + */ + path( + "/api/library", + () -> { + get("/{isbn}", this::bookByIsbn); + + /* + * Author by Id. + * + * @param id ID. + * @return An author + * @tag Author. Oxxx + * @operationId author + */ + get( + "/{id}", + ctx -> { + var id = ctx.path("id").value(); + return new Author(); + }); + + /* + * Query books. + * + * @param query Book's param query. + * @return Matching books. + * @x-badges.-name Beta + * @x-badges.position before + * @x-badges.color purple + * @operationId query + */ + get( + "/", + ctx -> { + List result = new ArrayList<>(); + var query = ctx.query(BookQuery.class); + return result; // List.of(new Book()); + }); + + /* + * Creates a new book. + * + *

Book can be created or updated. + * + * @param book Book to create. + * @return Saved book. + * @tag Author + * @operationId createBook + */ + post( + "/", + ctx -> { + var book = ctx.body(Book.class); + return book; + }); + }); + } + + /* + * Find a book by isbn. + * + * @param isbn Book isbn. Like IK-1900. + * @return A matching book. + * @throws NotFoundException 404 If a book doesn't exist. + * @throws BadRequestException 400 For bad ISBN code. + * @tag Book + * @tag Author + * @operationId bookByIsbn + */ + private Book bookByIsbn(Context ctx) { + var isbn = ctx.path("isbn").value(); + return new Book(); + } +} diff --git a/modules/jooby-openapi/src/test/java/javadoc/ExtensionJavaDocParserTest.java b/modules/jooby-openapi/src/test/java/javadoc/ExtensionJavaDocParserTest.java index fa0b1e048e..c4d49a21e7 100644 --- a/modules/jooby-openapi/src/test/java/javadoc/ExtensionJavaDocParserTest.java +++ b/modules/jooby-openapi/src/test/java/javadoc/ExtensionJavaDocParserTest.java @@ -5,7 +5,7 @@ */ package javadoc; -import static io.jooby.internal.openapi.javadoc.MiniYamlDocParser.parse; +import static io.jooby.internal.openapi.javadoc.TinyYamlDocParser.parse; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.List; diff --git a/modules/jooby-openapi/src/test/java/javadoc/JavaDocParserTest.java b/modules/jooby-openapi/src/test/java/javadoc/JavaDocParserTest.java index 1d92be4ab3..a8dd91ae0a 100644 --- a/modules/jooby-openapi/src/test/java/javadoc/JavaDocParserTest.java +++ b/modules/jooby-openapi/src/test/java/javadoc/JavaDocParserTest.java @@ -16,14 +16,218 @@ import org.junit.jupiter.api.Test; import io.jooby.SneakyThrows; -import io.jooby.internal.openapi.javadoc.ClassDoc; -import io.jooby.internal.openapi.javadoc.FieldDoc; -import io.jooby.internal.openapi.javadoc.JavaDocParser; -import io.jooby.internal.openapi.javadoc.MethodDoc; +import io.jooby.internal.openapi.javadoc.*; import issues.i3729.api.Book; public class JavaDocParserTest { + @Test + public void inheritance() throws Exception { + withDoc( + javadoc.input.Subclass.class, + doc -> { + assertEquals("Subclass summary.", doc.getSummary()); + assertEquals("Subclass description.", doc.getDescription()); + + assertEquals("Number on subclass.", doc.getPropertyDoc("number")); + assertEquals("Name on subclass.", doc.getPropertyDoc("name")); + assertEquals("Desc on base class.", doc.getPropertyDoc("description")); + }); + + withDoc( + javadoc.input.sub.Base.class, + doc -> { + assertEquals("Base summary.", doc.getSummary()); + assertEquals("Base description.", doc.getDescription()); + + assertNull(doc.getPropertyDoc("number")); + assertEquals("Name on base class.", doc.getPropertyDoc("name")); + assertEquals("Desc on base class.", doc.getPropertyDoc("description")); + }); + } + + @Test + public void multiline() throws Exception { + withDoc( + javadoc.input.MultilineComment.class, + doc -> { + assertNull(doc.getSummary()); + assertNull(doc.getDescription()); + + withScript( + doc, + "GET", + "/multiline", + method -> { + assertEquals("Multiline comment.", method.getSummary()); + assertEquals("multilineComment", method.getOperationId()); + assertEquals("Description in next line.", method.getDescription()); + assertNull(method.getReturnDoc()); + assertEquals("Path ID.", method.getParameterDoc("id")); + }); + }); + } + + @Test + public void lambdaDoc() throws Exception { + withDoc( + javadoc.input.LambdaRefApp.class, + doc -> { + assertEquals("LambdaRefApp", doc.getSimpleName()); + assertEquals("javadoc.input.LambdaRefApp", doc.getName()); + assertEquals("Lambda App.", doc.getSummary()); + assertEquals("Using method ref.", doc.getDescription()); + + withScript( + doc, + "GET", + "/reference", + method -> { + assertEquals("Find pet by id.", method.getSummary()); + assertEquals("findPetById", method.getOperationId()); + assertNull(method.getDescription()); + assertNull(method.getReturnDoc()); + assertEquals("Pet ID.", method.getParameterDoc("id")); + }); + + withScript( + doc, + "POST", + "/static-reference", + method -> { + assertEquals("Static reference.", method.getSummary()); + assertEquals("staticFindPetById", method.getOperationId()); + assertEquals("Path ID.", method.getParameterDoc("id")); + assertEquals("Description in next line.", method.getDescription()); + assertNull(method.getReturnDoc()); + }); + + withScript( + doc, + "PUT", + "/external-reference", + method -> { + assertEquals("External doc.", method.getSummary()); + assertEquals("external", method.getOperationId()); + assertNull(method.getDescription()); + assertNull(method.getReturnDoc()); + }); + + withScript( + doc, + "GET", + "/external-subPackage-reference", + method -> { + assertEquals("Sub package doc.", method.getSummary()); + assertEquals("subPackage", method.getOperationId()); + assertNull(method.getDescription()); + assertNull(method.getReturnDoc()); + }); + }); + } + + @Test + public void scriptDoc() throws Exception { + withDoc( + javadoc.input.ScriptApp.class, + doc -> { + assertEquals("ScriptApp", doc.getSimpleName()); + assertEquals("javadoc.input.ScriptApp", doc.getName()); + assertEquals("Script App.", doc.getSummary()); + assertEquals("Some description.", doc.getDescription()); + + withScript( + doc, + "GET", + "/static", + method -> { + assertEquals("This is a static path.", method.getSummary()); + assertEquals("No parameters", method.getDescription()); + assertEquals("Request Path.", method.getReturnDoc()); + }); + + withScript( + doc, + "DELETE", + "/{id}", + method -> { + assertEquals("Delete something.", method.getSummary()); + assertEquals("ID to delete.", method.getParameterDoc("id")); + }); + + withScript( + doc, + "GET", + "/{id}", + method -> { + assertEquals("Path param.", method.getSummary()); + assertNull(method.getDescription()); + assertEquals("Some value.", method.getReturnDoc()); + assertEquals("Path ID.", method.getParameterDoc("id")); + }); + + withScript( + doc, + "GET", + "/tree/folder/{id}", + method -> { + assertNotNull(method.getPath()); + assertEquals("Tree summary.", method.getPath().getSummary()); + assertEquals("Tree doc.", method.getPath().getDescription()); + assertNotNull(method.getPath().getTags()); + assertEquals(1, method.getPath().getTags().size()); + assertEquals("Tree", method.getPath().getTags().getFirst().getName()); + + assertEquals("Item doc.", method.getSummary()); + assertNull(method.getDescription()); + }); + + withScript( + doc, + "GET", + "/tree/folder", + method -> { + assertEquals("Items.", method.getSummary()); + assertNull(method.getDescription()); + }); + + withScript( + doc, + "GET", + "/tree/file/{fileId}", + method -> { + assertEquals("Sub Items.", method.getSummary()); + assertNull(method.getDescription()); + }); + + withScript( + doc, + "GET", + "/tree/mount", + method -> { + assertEquals("Mounted.", method.getSummary()); + assertNull(method.getDescription()); + }); + + withScript( + doc, + "POST", + "/routes", + method -> { + assertEquals("Routes.", method.getSummary()); + assertNull(method.getDescription()); + }); + withScript( + doc, + "GET", + "/nested/last", + method -> { + assertEquals("Last.", method.getSummary()); + assertNull(method.getDescription()); + }); + }); + } + @Test public void apiDoc() throws Exception { withDoc( @@ -47,7 +251,7 @@ public void apiDoc() throws Exception { withMethod( doc, "hello", - List.of("name", "age", "list", "str"), + List.of("java.util.List", "int", "java.util.List", "java.lang.String"), method -> { assertEquals("This is the Hello /endpoint", method.getSummary()); assertEquals("Operation description", method.getDescription()); @@ -61,7 +265,7 @@ public void apiDoc() throws Exception { withMethod( doc, "search", - List.of("query"), + List.of("javadoc.input.QueryBeanDoc"), method -> { assertEquals("Search database.", method.getSummary()); assertEquals("Search DB", method.getDescription()); @@ -74,7 +278,7 @@ public void apiDoc() throws Exception { withMethod( doc, "recordBean", - List.of("query"), + List.of("javadoc.input.RecordBeanDoc"), method -> { assertEquals("Record database.", method.getSummary()); assertNull(method.getDescription()); @@ -85,7 +289,7 @@ public void apiDoc() throws Exception { withMethod( doc, "enumParam", - List.of("query"), + List.of("javadoc.input.EnumDoc"), method -> { assertEquals("Enum database.", method.getSummary()); assertEquals("Enum doc.", method.getParameterDoc("query")); @@ -116,7 +320,7 @@ public void noClassDoc() throws Exception { withMethod( doc, "hello", - List.of("name"), + List.of("java.lang.String"), methodDoc -> { assertEquals("Method Doc.", methodDoc.getSummary()); assertNull(methodDoc.getDescription()); @@ -258,6 +462,13 @@ private void withMethod( consumer.accept(method.get()); } + private void withScript( + ClassDoc doc, String method, String pattern, Consumer consumer) { + var script = doc.getScript(method, pattern); + assertTrue(script.isPresent()); + consumer.accept(script.get()); + } + private void withField(ClassDoc doc, String name, Consumer consumer) { var method = doc.getField(name); assertTrue(method.isPresent()); diff --git a/modules/jooby-openapi/src/test/java/javadoc/PrintAstTree.java b/modules/jooby-openapi/src/test/java/javadoc/PrintAstTree.java index 4cb088c810..4761e372e9 100644 --- a/modules/jooby-openapi/src/test/java/javadoc/PrintAstTree.java +++ b/modules/jooby-openapi/src/test/java/javadoc/PrintAstTree.java @@ -9,7 +9,7 @@ import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; -import javadoc.input.ApiDoc; +import javadoc.input.Subclass; import com.puppycrawl.tools.checkstyle.AstTreeStringPrinter; import com.puppycrawl.tools.checkstyle.api.CheckstyleException; @@ -25,7 +25,7 @@ public static void main(String[] args) throws CheckstyleException, IOException { .resolve("java"); var stringAst = AstTreeStringPrinter.printJavaAndJavadocTree( - baseDir.resolve(toPath(ApiDoc.class)).toFile()); + baseDir.resolve(toPath(Subclass.class)).toFile()); System.out.println(stringAst); } diff --git a/modules/jooby-openapi/src/test/java/javadoc/input/LambdaRefApp.java b/modules/jooby-openapi/src/test/java/javadoc/input/LambdaRefApp.java new file mode 100644 index 0000000000..fe77babc81 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/javadoc/input/LambdaRefApp.java @@ -0,0 +1,47 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package javadoc.input; + +import javadoc.input.sub.SubPackageHandler; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.Jooby; + +/** + * Lambda App. + * + *

Using method ref. + */ +public class LambdaRefApp extends Jooby { + { + get("/reference", this::findPetById); + post("/static-reference", javadoc.input.LambdaRefApp::staticFindPetById); + put("/external-reference", RequestHandler::external); + get("/external-subPackage-reference", SubPackageHandler::subPackage); + } + + /* + * Find pet by id. + * + * @param id Pet ID. + */ + private @NonNull String findPetById(Context ctx) { + var id = ctx.path("id").value(); + return "Pets"; + } + + /* + Static reference. + + Description in next line. + @param id Path ID. + */ + private static @NonNull String staticFindPetById(Context ctx) { + var id = ctx.path("id").value(); + return "Pets"; + } +} diff --git a/modules/jooby-openapi/src/test/java/javadoc/input/MultilineComment.java b/modules/jooby-openapi/src/test/java/javadoc/input/MultilineComment.java new file mode 100644 index 0000000000..f62552a84c --- /dev/null +++ b/modules/jooby-openapi/src/test/java/javadoc/input/MultilineComment.java @@ -0,0 +1,28 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package javadoc.input; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.Jooby; + +public class MultilineComment extends Jooby { + { + get("/multiline", this::multilineComment); + } + + /* + Multiline comment. + + Description in next + line. + @param id Path ID. + */ + private @NonNull String multilineComment(Context ctx) { + var id = ctx.path("id").value(); + return "Pets"; + } +} diff --git a/modules/jooby-openapi/src/test/java/javadoc/input/RequestHandler.java b/modules/jooby-openapi/src/test/java/javadoc/input/RequestHandler.java new file mode 100644 index 0000000000..e3bdd4f4ee --- /dev/null +++ b/modules/jooby-openapi/src/test/java/javadoc/input/RequestHandler.java @@ -0,0 +1,18 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package javadoc.input; + +import io.jooby.Context; + +public class RequestHandler { + + /* + * External doc. + */ + public static String external(Context ctx) { + return ctx.path("external").value(); + } +} diff --git a/modules/jooby-openapi/src/test/java/javadoc/input/ScriptApp.java b/modules/jooby-openapi/src/test/java/javadoc/input/ScriptApp.java new file mode 100644 index 0000000000..16726d15d8 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/javadoc/input/ScriptApp.java @@ -0,0 +1,101 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package javadoc.input; + +import java.util.function.Predicate; + +import io.jooby.Context; +import io.jooby.Jooby; + +/** Script App. Some description. */ +public class ScriptApp extends Jooby { + { + // Ignored + /* + * This is a static path. No parameters + * + * @return Request Path. + */ + get( + "/static", + ctx -> { + // Ignored. + return ctx.getRequestPath(); + // Ignored + }); + + // Ignored + /* + * Path param. + * + * @param id Path ID. + * @return Some value. + */ + // Ignored + get("/{id}", Context::getRequestPath); + + /** + * Delete something. + * + * @param id ID to delete. + */ + delete("/{id}", Context::getRequestPath); + + /* + * Tree summary. + * + * Tree doc. + * + * @tag Tree + */ + path( + "/tree", + () -> { + /* + * Item doc. + */ + get("/folder/{id}", Context::getRequestPath); + + /* + * Items. + */ + get("/folder", Context::getRequestPath); + + path( + "/file", + () -> { + /* + * Sub Items. + */ + get("/{fileId}", Context::getRequestPath); + }); + mount( + Predicate.isEqual("/folder/{folderId}"), + () -> { + /* + * Mounted. + */ + get("/mount", Context::getRequestPath); + }); + }); + + routes( + () -> { + /* + * Routes. + */ + post("/routes", Context::getRequestPath); + path( + "/nested", + () -> { + /* + * Last. + */ + get("/last", Context::getRequestPath); + }); + }); + } +} diff --git a/modules/jooby-openapi/src/test/java/javadoc/input/Subclass.java b/modules/jooby-openapi/src/test/java/javadoc/input/Subclass.java new file mode 100644 index 0000000000..f169ba3a47 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/javadoc/input/Subclass.java @@ -0,0 +1,36 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package javadoc.input; + +import javadoc.input.sub.Base; + +/** + * Subclass summary. + * + *

Subclass description. + */ +public class Subclass extends Base { + private int number; + + /** + * Number on subclass. + * + * @return Number on subclass. + */ + public int getNumber() { + return number; + } + + /** + * Name on subclass. + * + * @return Name on subclass. + */ + @Override + public String getName() { + return super.getName(); + } +} diff --git a/modules/jooby-openapi/src/test/java/javadoc/input/sub/Base.java b/modules/jooby-openapi/src/test/java/javadoc/input/sub/Base.java new file mode 100644 index 0000000000..b878717e46 --- /dev/null +++ b/modules/jooby-openapi/src/test/java/javadoc/input/sub/Base.java @@ -0,0 +1,32 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package javadoc.input.sub; + +/** + * Base summary. + * + *

Base description. + */ +public class Base { + + private String name; + + /** Desc on base class. */ + private String description; + + /** + * Name on base class. + * + * @return On base class. + */ + public String getName() { + return name; + } + + public String getDescription() { + return description; + } +} diff --git a/modules/jooby-openapi/src/test/java/javadoc/input/sub/SubPackageHandler.java b/modules/jooby-openapi/src/test/java/javadoc/input/sub/SubPackageHandler.java new file mode 100644 index 0000000000..28966c8d1e --- /dev/null +++ b/modules/jooby-openapi/src/test/java/javadoc/input/sub/SubPackageHandler.java @@ -0,0 +1,20 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package javadoc.input.sub; + +import io.jooby.Context; + +public class SubPackageHandler { + + /* + * Sub package doc. + * + * @param external External parameter. + */ + public static String subPackage(Context ctx) { + return ctx.path("external").value(); + } +}