Skip to content

Commit ee8ee40

Browse files
committed
Merge branch 'openapi' of github.com:SentryMan/avaje-http into SentryMan-openapi
2 parents 9f5805d + df6d0c8 commit ee8ee40

File tree

9 files changed

+268
-13
lines changed

9 files changed

+268
-13
lines changed

http-generator-core/src/main/java/io/avaje/http/generator/core/MethodReader.java

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.Arrays;
66
import java.util.List;
77
import java.util.Optional;
8+
import java.util.function.Predicate;
89
import java.util.stream.Collectors;
910
import java.util.stream.Stream;
1011

@@ -60,6 +61,7 @@ public class MethodReader {
6061

6162
private final PathSegments pathSegments;
6263
private final boolean hasValid;
64+
private final List<ExecutableElement> superMethods;
6365

6466
MethodReader(ControllerReader bean, ExecutableElement element, ExecutableType actualExecutable, ProcessingContext ctx) {
6567
this.ctx = ctx;
@@ -69,18 +71,48 @@ public class MethodReader {
6971
this.actualParams = (actualExecutable == null) ? null : actualExecutable.getParameterTypes();
7072
this.isVoid = element.getReturnType().getKind() == TypeKind.VOID;
7173
this.methodRoles = Util.findRoles(element);
72-
this.javadoc = Javadoc.parse(ctx.docComment(element));
7374
this.produces = produces(bean);
74-
this.apiResponses = getApiResponses();
7575
initWebMethodViaAnnotation();
76+
77+
this.superMethods =
78+
ctx.getSuperMethods(element.getEnclosingElement(), element.getSimpleName().toString());
79+
80+
superMethods.stream().forEach(m -> methodRoles.addAll(Util.findRoles(m)));
81+
82+
this.apiResponses = getApiResponses();
83+
this.javadoc =
84+
Optional.of(Javadoc.parse(ctx.docComment(element)))
85+
.filter(Predicate.not(Javadoc::isEmpty))
86+
.orElseGet(
87+
() ->
88+
superMethods.stream()
89+
.map(e -> Javadoc.parse(ctx.docComment(e)))
90+
.filter(Predicate.not(Javadoc::isEmpty))
91+
.findFirst()
92+
.orElse(Javadoc.parse("")));
93+
7694
if (isWebMethod()) {
77-
Annotation jakartaValidAnnotation = null;
95+
96+
Class<Annotation> jakartaValidAnnotation;
7897
try {
79-
jakartaValidAnnotation = findAnnotation(jakartaValidAnnotation());
98+
jakartaValidAnnotation = jakartaValidAnnotation();
8099
} catch (final ClassNotFoundException e) {
81-
// ignore
100+
jakartaValidAnnotation = null;
82101
}
83-
this.hasValid = findAnnotation(Valid.class) != null || jakartaValidAnnotation != null;
102+
103+
final var jakartaAnnotation = jakartaValidAnnotation;
104+
105+
this.hasValid =
106+
findAnnotation(Valid.class) != null
107+
|| (jakartaValidAnnotation != null && findAnnotation(jakartaValidAnnotation) != null)
108+
|| superMethods.stream()
109+
.map(
110+
e ->
111+
findAnnotation(Valid.class, e) != null
112+
|| (jakartaAnnotation != null
113+
&& findAnnotation(jakartaAnnotation, e) != null))
114+
.anyMatch(b -> b);
115+
84116
this.pathSegments = PathSegments.parse(Util.combinePath(bean.path(), webMethodPath));
85117
} else {
86118
this.hasValid = false;
@@ -149,19 +181,35 @@ private List<OpenAPIResponse> getApiResponses() {
149181
.map(OpenAPIResponses::value)
150182
.flatMap(Arrays::stream);
151183

152-
return Stream.concat(container, Arrays.stream(element.getAnnotationsByType(OpenAPIResponse.class)))
153-
.collect(Collectors.toList());
184+
final var methodResponses =
185+
Stream.concat(
186+
container, Arrays.stream(element.getAnnotationsByType(OpenAPIResponse.class)));
154187

188+
final var superMethodResponses =
189+
superMethods.stream()
190+
.flatMap(
191+
m ->
192+
Stream.concat(
193+
Optional.ofNullable(findAnnotation(OpenAPIResponses.class, m)).stream()
194+
.map(OpenAPIResponses::value)
195+
.flatMap(Arrays::stream),
196+
Arrays.stream(m.getAnnotationsByType(OpenAPIResponse.class))));
155197

198+
return Stream.concat(methodResponses, superMethodResponses).collect(Collectors.toList());
156199
}
157200

158201
public <A extends Annotation> A findAnnotation(Class<A> type) {
159-
A annotation = element.getAnnotation(type);
202+
203+
return findAnnotation(type, element);
204+
}
205+
206+
public <A extends Annotation> A findAnnotation(Class<A> type, ExecutableElement elem) {
207+
final var annotation = elem.getAnnotation(type);
160208
if (annotation != null) {
161209
return annotation;
162210
}
163211

164-
return bean.findMethodAnnotation(type, element);
212+
return bean.findMethodAnnotation(type, elem);
165213
}
166214

167215
private List<String> addTagsToList(Element element, List<String> list) {
@@ -179,8 +227,9 @@ private List<String> addTagsToList(Element element, List<String> list) {
179227
}
180228

181229
public List<String> tags() {
182-
List<String> tags = new ArrayList<>();
183-
tags = addTagsToList(element, tags);
230+
final var tags = addTagsToList(element, new ArrayList<>());
231+
superMethods.forEach(e -> addTagsToList(e, tags));
232+
184233
return addTagsToList(element.getEnclosingElement(), tags);
185234
}
186235

http-generator-core/src/main/java/io/avaje/http/generator/core/ProcessingContext.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package io.avaje.http.generator.core;
22

33
import java.io.IOException;
4+
import java.util.List;
5+
import java.util.Objects;
6+
import java.util.stream.Collectors;
47

58
import javax.annotation.processing.Filer;
69
import javax.annotation.processing.Messager;
710
import javax.annotation.processing.ProcessingEnvironment;
811
import javax.lang.model.element.Element;
12+
import javax.lang.model.element.ExecutableElement;
913
import javax.lang.model.element.TypeElement;
1014
import javax.lang.model.type.DeclaredType;
1115
import javax.lang.model.type.TypeMirror;
16+
import javax.lang.model.util.ElementFilter;
1217
import javax.lang.model.util.Elements;
1318
import javax.lang.model.util.Types;
1419
import javax.tools.Diagnostic;
@@ -115,6 +120,25 @@ public TypeMirror asMemberOf(DeclaredType declaredType, Element element) {
115120
return types.asMemberOf(declaredType, element);
116121
}
117122

123+
public List<ExecutableElement> getSuperMethods(Element element, String methodName) {
124+
125+
return types.directSupertypes(element.asType()).stream()
126+
.filter(t -> !t.toString().contains("java.lang.Object"))
127+
.map(
128+
superType -> {
129+
final var superClass = (TypeElement) types.asElement(superType);
130+
for (final ExecutableElement method :
131+
ElementFilter.methodsIn(elements.getAllMembers(superClass))) {
132+
if (method.getSimpleName().contentEquals(methodName)) {
133+
return method;
134+
}
135+
}
136+
return null;
137+
})
138+
.filter(Objects::nonNull)
139+
.collect(Collectors.toList());
140+
}
141+
118142
public PlatformAdapter platform() {
119143
return readAdapter;
120144
}

http-generator-core/src/main/java/io/avaje/http/generator/core/javadoc/Javadoc.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,12 @@ public String getReturnDescription() {
6363
public boolean isDeprecated() {
6464
return deprecated;
6565
}
66+
67+
public boolean isEmpty() {
68+
return summary.isBlank()
69+
&& description.isBlank()
70+
&& params.isEmpty()
71+
&& returnDescription.isBlank()
72+
&& !deprecated;
73+
}
6674
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.example.myapp.web.test;
2+
3+
import io.avaje.http.api.Get;
4+
import io.avaje.http.api.MediaType;
5+
import io.avaje.http.api.OpenAPIResponse;
6+
import io.avaje.http.api.Path;
7+
import io.avaje.http.api.Produces;
8+
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
9+
import io.swagger.v3.oas.annotations.info.Info;
10+
import io.swagger.v3.oas.annotations.tags.Tag;
11+
12+
@Path("javalin")
13+
@OpenAPIDefinition(
14+
info =
15+
@Info(
16+
title = "Example service showing off the Path extension method of controller",
17+
description = ""))
18+
public interface HealthController {
19+
/**
20+
* Standard Get
21+
*
22+
* @return a health check
23+
*/
24+
@Get("/health")
25+
@Produces(MediaType.TEXT_PLAIN)
26+
@Tag(name = "tag1", description = "it's somethin")
27+
@OpenAPIResponse(responseCode = "500", type = ErrorResponse.class)
28+
String health();
29+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.example.myapp.web.test;
2+
3+
import io.avaje.http.api.Controller;
4+
5+
@Controller
6+
public class HealthControllerImpl implements HealthController {
7+
8+
@Override
9+
public String health() {
10+
11+
return "this feels like a picnic *chew*";
12+
}
13+
}

tests/test-javalin-jsonb/src/main/resources/public/openapi.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
"version" : ""
77
},
88
"tags" : [
9+
{
10+
"name" : "tag1",
11+
"description" : "it's somethin"
12+
},
913
{
1014
"name" : "tag1",
1115
"description" : "this is added to openapi tags"
@@ -764,6 +768,37 @@
764768
"deprecated" : true
765769
}
766770
},
771+
"/javalin/health" : {
772+
"get" : {
773+
"tags" : [
774+
"tag1"
775+
],
776+
"summary" : "Standard Get",
777+
"description" : "",
778+
"responses" : {
779+
"500" : {
780+
"description" : "a health check",
781+
"content" : {
782+
"text/plain" : {
783+
"schema" : {
784+
"$ref" : "#/components/schemas/ErrorResponse"
785+
}
786+
}
787+
}
788+
},
789+
"200" : {
790+
"description" : "a health check",
791+
"content" : {
792+
"text/plain" : {
793+
"schema" : {
794+
"type" : "string"
795+
}
796+
}
797+
}
798+
}
799+
}
800+
}
801+
},
767802
"/openapi/get" : {
768803
"get" : {
769804
"tags" : [

tests/test-javalin-jsonb/src/test/java/io/avaje/http/generator/JavalinProcessorTest.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.nio.file.Files;
99
import java.nio.file.Path;
1010
import java.nio.file.Paths;
11+
import java.util.ArrayList;
1112
import java.util.Collections;
1213
import java.util.Comparator;
1314
import java.util.List;
@@ -155,12 +156,47 @@ public void testOpenAPIGeneration() throws Exception {
155156

156157
final var mapper = new ObjectMapper();
157158
final var expectedOpenApiJson =
158-
mapper.readTree(new File("src/test/java/io/avaje/http/generator/expectedOpenApi.json"));
159+
mapper.readTree(new File("src/test/resources/expectedOpenApi.json"));
159160
final var generatedOpenApi = mapper.readTree(new File("openapi.json"));
160161

161162
assert expectedOpenApiJson.equals(generatedOpenApi);
162163
}
163164

165+
@Test
166+
public void testInheritableOpenAPIGeneration() throws Exception {
167+
final var source = Paths.get("src").toAbsolutePath().toString();
168+
// OpenAPIController
169+
final var files = getSourceFiles(source);
170+
171+
final List<JavaFileObject> openAPIController = new ArrayList<>(2);
172+
for (final var file : files) {
173+
if (file.isNameCompatible("HealthController", Kind.SOURCE)
174+
|| file.isNameCompatible("HealthControllerImpl", Kind.SOURCE))
175+
openAPIController.add(file);
176+
}
177+
final var compiler = ToolProvider.getSystemJavaCompiler();
178+
179+
final var task =
180+
compiler.getTask(
181+
new PrintWriter(System.out),
182+
null,
183+
null,
184+
List.of("--release=11"),
185+
null,
186+
openAPIController);
187+
task.setProcessors(List.of(new JavalinProcessor(false), new Processor()));
188+
189+
assertThat(task.call()).isTrue();
190+
191+
final var mapper = new ObjectMapper();
192+
final var expectedOpenApiJson =
193+
mapper.readTree(new File("src/test/resources/expectedInheritedOpenApi.json"));
194+
final var generatedOpenApi = mapper.readTree(new File("openapi.json"));
195+
196+
assert expectedOpenApiJson.equals(generatedOpenApi);
197+
}
198+
199+
164200
private Iterable<JavaFileObject> getSourceFiles(String source) throws Exception {
165201
final var compiler = ToolProvider.getSystemJavaCompiler();
166202
final var files = compiler.getStandardFileManager(null, null, null);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"openapi" : "3.0.1",
3+
"info" : {
4+
"title" : "Example service showing off the Path extension method of controller",
5+
"version" : ""
6+
},
7+
"tags" : [
8+
{
9+
"name" : "tag1",
10+
"description" : "it's somethin"
11+
}
12+
],
13+
"paths" : {
14+
"/javalin/health" : {
15+
"get" : {
16+
"tags" : [
17+
"tag1"
18+
],
19+
"summary" : "Standard Get",
20+
"description" : "",
21+
"responses" : {
22+
"500" : {
23+
"description" : "a health check",
24+
"content" : {
25+
"text/plain" : {
26+
"schema" : {
27+
"$ref" : "#/components/schemas/ErrorResponse"
28+
}
29+
}
30+
}
31+
},
32+
"200" : {
33+
"description" : "a health check",
34+
"content" : {
35+
"text/plain" : {
36+
"schema" : {
37+
"type" : "string"
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}
44+
}
45+
},
46+
"components" : {
47+
"schemas" : {
48+
"ErrorResponse" : {
49+
"type" : "object",
50+
"properties" : {
51+
"id" : {
52+
"type" : "string"
53+
},
54+
"text" : {
55+
"type" : "string"
56+
}
57+
}
58+
}
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)