Skip to content

Commit b4a3ccf

Browse files
authored
Merge pull request #115 from SentryMan/openapi
Enhance OpenAPI generation
2 parents b9b49b2 + e4343fb commit b4a3ccf

File tree

10 files changed

+430
-27
lines changed

10 files changed

+430
-27
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# avaje-http
22

3-
Http server and client libraries and code generation.
3+
HTTP server and client libraries via code generation.
44

5+
Documentation at [avaje.io/http](https://avaje.io/http/)
56
## Http Server
67

78
A jax-rs style controllers with annotations (`@Path`, `@Get` ...)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.avaje.http.api;
2+
3+
import static java.lang.annotation.ElementType.METHOD;
4+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
5+
6+
import java.lang.annotation.Repeatable;
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.Target;
9+
10+
/**
11+
* Specify endpoint response status code/description/type.
12+
*
13+
* <p>When not specified the default 2xx openAPI generation is based on the javadoc of the method.
14+
* <p> Will not override the default 2xx generated openapi unless status code is 2xx
15+
* <pre>{@code
16+
* @Post("/post")
17+
* @OpenAPIReturns(responseCode = "200", description = "from annotaion")
18+
* @OpenAPIReturns(responseCode = "201")
19+
* @OpenAPIReturns(responseCode = "500", description = "Some other Error", type=ErrorResponse.class)
20+
* ResponseModel endpoint() {}
21+
*
22+
* }</pre>
23+
*/
24+
@Target(value = METHOD)
25+
@Retention(value = RUNTIME)
26+
@Repeatable(OpenAPIResponses.class)
27+
public @interface OpenAPIResponse {
28+
29+
/** the http status code of this response */
30+
String responseCode();
31+
32+
/**
33+
* The description of the return value. By default uses the @return javadoc of the method as the
34+
* description
35+
*/
36+
String description() default "";
37+
38+
/**
39+
* The concrete type that that this endpoint returns. If status code is a 2xx code it will default
40+
* to the return type of the method
41+
*/
42+
Class<?> type() default Void.class;
43+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.avaje.http.api;
2+
3+
import static java.lang.annotation.ElementType.METHOD;
4+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
5+
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.Target;
8+
9+
/**
10+
* Container for repeatable {@link OpenAPIResponse} annotation
11+
*
12+
* @see OpenAPIResponse
13+
*/
14+
@Target(value = METHOD)
15+
@Retention(value = RUNTIME)
16+
public @interface OpenAPIResponses {
17+
OpenAPIResponse[] value();
18+
}

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

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

3+
import java.lang.annotation.Annotation;
4+
import java.util.ArrayList;
5+
import java.util.Arrays;
6+
import java.util.List;
7+
import java.util.Optional;
8+
import java.util.stream.Collectors;
9+
import java.util.stream.Stream;
10+
11+
import javax.lang.model.element.Element;
12+
import javax.lang.model.element.ExecutableElement;
13+
import javax.lang.model.element.VariableElement;
14+
import javax.lang.model.type.ExecutableType;
15+
import javax.lang.model.type.TypeKind;
16+
import javax.lang.model.type.TypeMirror;
17+
import javax.validation.Valid;
18+
319
import io.avaje.http.api.Delete;
420
import io.avaje.http.api.Form;
521
import io.avaje.http.api.Get;
22+
import io.avaje.http.api.OpenAPIResponse;
23+
import io.avaje.http.api.OpenAPIResponses;
624
import io.avaje.http.api.Patch;
725
import io.avaje.http.api.Post;
826
import io.avaje.http.api.Produces;
927
import io.avaje.http.api.Put;
1028
import io.avaje.http.generator.core.javadoc.Javadoc;
1129
import io.avaje.http.generator.core.openapi.MethodDocBuilder;
30+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1231
import io.swagger.v3.oas.annotations.tags.Tag;
1332
import io.swagger.v3.oas.annotations.tags.Tags;
1433

15-
import javax.lang.model.element.Element;
16-
import javax.lang.model.element.ExecutableElement;
17-
import javax.lang.model.element.VariableElement;
18-
import javax.lang.model.type.ExecutableType;
19-
import javax.lang.model.type.TypeKind;
20-
import javax.lang.model.type.TypeMirror;
21-
import javax.validation.Valid;
22-
import java.lang.annotation.Annotation;
23-
import java.util.ArrayList;
24-
import java.util.List;
25-
2634
public class MethodReader {
2735

2836
private final ProcessingContext ctx;
@@ -46,13 +54,19 @@ public class MethodReader {
4654

4755
private final String produces;
4856

57+
private final List<OpenAPIResponse> apiResponses;
58+
4959
private final ExecutableType actualExecutable;
5060
private final List<? extends TypeMirror> actualParams;
5161

5262
private final PathSegments pathSegments;
5363
private final boolean hasValid;
5464

55-
MethodReader(ControllerReader bean, ExecutableElement element, ExecutableType actualExecutable, ProcessingContext ctx) {
65+
MethodReader(
66+
ControllerReader bean,
67+
ExecutableElement element,
68+
ExecutableType actualExecutable,
69+
ProcessingContext ctx) {
5670
this.ctx = ctx;
5771
this.bean = bean;
5872
this.element = element;
@@ -62,6 +76,7 @@ public class MethodReader {
6276
this.methodRoles = Util.findRoles(element);
6377
this.javadoc = Javadoc.parse(ctx.getDocComment(element));
6478
this.produces = produces(bean);
79+
this.apiResponses = getApiResponses();
6580
initWebMethodViaAnnotation();
6681
if (isWebMethod()) {
6782
this.hasValid = findAnnotation(Valid.class) != null;
@@ -118,10 +133,22 @@ public Javadoc javadoc() {
118133
}
119134

120135
private String produces(ControllerReader bean) {
121-
final Produces produces = findAnnotation(Produces.class);
136+
final var produces = findAnnotation(Produces.class);
122137
return (produces != null) ? produces.value() : bean.produces();
123138
}
124139

140+
private List<OpenAPIResponse> getApiResponses() {
141+
final var container =
142+
Optional.ofNullable(findAnnotation(OpenAPIResponses.class)).stream()
143+
.map(OpenAPIResponses::value)
144+
.flatMap(Arrays::stream);
145+
146+
return Stream.concat(container, Arrays.stream(element.getAnnotationsByType(OpenAPIResponse.class)))
147+
.collect(Collectors.toList());
148+
149+
150+
}
151+
125152
public <A extends Annotation> A findAnnotation(Class<A> type) {
126153
A annotation = element.getAnnotation(type);
127154
if (annotation != null) {
@@ -216,6 +243,10 @@ public String produces() {
216243
return produces;
217244
}
218245

246+
public List<OpenAPIResponse> apiResponses() {
247+
return apiResponses;
248+
}
249+
219250
public TypeMirror returnType() {
220251
if (actualExecutable != null) {
221252
return actualExecutable.getReturnType();
@@ -273,5 +304,4 @@ public String bodyName() {
273304
}
274305
return "body";
275306
}
276-
277307
}

http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MethodDocBuilder.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package io.avaje.http.generator.core.openapi;
22

3+
import javax.lang.model.type.MirroredTypeException;
4+
import javax.lang.model.type.TypeMirror;
5+
36
import io.avaje.http.api.MediaType;
47
import io.avaje.http.generator.core.MethodParam;
58
import io.avaje.http.generator.core.MethodReader;
@@ -73,16 +76,46 @@ public void build() {
7376
ApiResponse response = new ApiResponse();
7477
response.setDescription(javadoc.getReturnDescription());
7578

79+
final var produces = methodReader.produces();
80+
final var contentMediaType = (produces == null) ? MediaType.APPLICATION_JSON : produces;
81+
7682
if (methodReader.isVoid()) {
7783
if (isEmpty(response.getDescription())) {
7884
response.setDescription("No content");
7985
}
8086
} else {
81-
final String produces = methodReader.produces();
82-
String contentMediaType = (produces == null) ? MediaType.APPLICATION_JSON : produces;
83-
response.setContent(ctx.createContent(methodReader.returnType(), contentMediaType));
87+
response.setContent(ctx.createContent(methodReader.returnType(), contentMediaType));
88+
}
89+
var override2xx = false;
90+
for (final var responseAnnotation : methodReader.apiResponses()) {
91+
final var newResponse = new ApiResponse();
92+
93+
if (responseAnnotation.description().isEmpty()) {
94+
newResponse.setDescription(response.getDescription());
95+
} else {
96+
newResponse.setDescription(responseAnnotation.description());
97+
}
98+
99+
// if user wants to define their own 2xx status code
100+
if (responseAnnotation.responseCode().startsWith("2")) {
101+
newResponse.setContent(response.getContent());
102+
override2xx = true;
103+
}
104+
TypeMirror returnType = null;
105+
try {
106+
// this will always throw
107+
responseAnnotation.type();
108+
} catch (final MirroredTypeException mte) {
109+
returnType = mte.getTypeMirror();
110+
}
111+
112+
if (!"java.lang.Void".equals(returnType.toString())) {
113+
newResponse.setContent(ctx.createContent(returnType, contentMediaType));
114+
}
115+
116+
responses.addApiResponse(responseAnnotation.responseCode(), newResponse);
84117
}
85-
responses.addApiResponse(methodReader.statusCode(), response);
118+
if (!override2xx) responses.addApiResponse(methodReader.statusCode(), response);
86119
}
87120

88121
DocContext getContext() {

http-generator-core/src/main/java/io/avaje/http/generator/core/openapi/MethodParamDocBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public MethodParamDocBuilder(MethodDocBuilder methodDoc, ElementReader param) {
3232
this.paramType = param.paramType();
3333
this.paramName = param.paramName();
3434
this.varName = param.varName();
35-
this.rawType = param.rawType();
35+
this.rawType = param.type().full();
3636
this.element = param.element();
3737
}
3838

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.example.myapp.web.test;
2+
3+
public class ErrorResponse {
4+
5+
public String id;
6+
public String text;
7+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.example.myapp.web.test;
2+
3+
import java.util.List;
4+
5+
import io.avaje.http.api.Controller;
6+
import io.avaje.http.api.Get;
7+
import io.avaje.http.api.MediaType;
8+
import io.avaje.http.api.OpenAPIResponse;
9+
import io.avaje.http.api.OpenAPIResponses;
10+
import io.avaje.http.api.Path;
11+
import io.avaje.http.api.Post;
12+
import io.avaje.http.api.Produces;
13+
import io.javalin.http.Context;
14+
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
15+
import io.swagger.v3.oas.annotations.info.Info;
16+
import io.swagger.v3.oas.annotations.tags.Tag;
17+
18+
@OpenAPIDefinition(
19+
info =
20+
@Info(
21+
title = "Example service",
22+
description = "Example Javalin controllers with Java and Maven"))
23+
@Controller
24+
@Path("openapi/")
25+
public class OpenAPIController {
26+
27+
/**
28+
* Example of Open API Get (up to the first period is the summary). When using Javalin Context
29+
* only <br>
30+
* This Javadoc description is added to the generated openapi.json
31+
*
32+
* @return funny phrase (this part of the javadoc is added to the response desc)
33+
*/
34+
@Get("/get")
35+
@Produces(MediaType.TEXT_PLAIN)
36+
@OpenAPIResponse(responseCode = "200", type = String.class)
37+
void ctxEndpoint(Context ctx) {
38+
ctx.contentType(MediaType.TEXT_PLAIN).result("healthlmao");
39+
}
40+
41+
/**
42+
* Standard Post. uses tag annotation to add tags to openapi json
43+
*
44+
* @param b the body (this is used for generated request body desc)
45+
* @return the response body (from javadoc)
46+
*/
47+
@Post("/post")
48+
@Tag(name = "tag1", description = "this is added to openapi tags")
49+
@OpenAPIResponse(responseCode = "200", description = "overrides @return javadoc description")
50+
@OpenAPIResponse(responseCode = "201")
51+
@OpenAPIResponse(
52+
responseCode = "400",
53+
description = "User not found (Will not have an associated response schema)")
54+
@OpenAPIResponse(
55+
responseCode = "500",
56+
description = "Some other Error (Will have this error class as the response class)",
57+
type = ErrorResponse.class)
58+
Person testPost(Person b) {
59+
return new Person(0, "baby");
60+
}
61+
62+
/**
63+
* Standard Post. The Deprecated annotation adds "deprecacted:true" to the generated json
64+
*
65+
* @param m the body
66+
* @return the response body (from javadoc)
67+
*/
68+
@Deprecated
69+
@Post("/post1")
70+
@OpenAPIResponses({
71+
@OpenAPIResponse(responseCode = "400", description = "User not found"),
72+
@OpenAPIResponse(
73+
responseCode = "500",
74+
description = "Some other Error",
75+
type = ErrorResponse.class)
76+
})
77+
Person testPostl(List<Person> m) {
78+
79+
return new Person(0, "baby");
80+
}
81+
}

0 commit comments

Comments
 (0)