Skip to content

Commit e9daa50

Browse files
author
bnasslahsen
committed
Initial support of Webflux with Functional Endpoints
1 parent ed410a8 commit e9daa50

File tree

6 files changed

+98
-23
lines changed

6 files changed

+98
-23
lines changed

springdoc-openapi-common/src/main/java/org/springdoc/api/AbstractOpenApiResource.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ protected synchronized OpenAPI getOpenApi() {
155155
protected abstract void getPaths(Map<String, Object> findRestControllers);
156156

157157
protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
158-
Set<RequestMethod> requestMethods) {
158+
Set<RequestMethod> requestMethods, String[] methodConsumes, String[] methodProduces) {
159159
OpenAPI openAPI = openAPIBuilder.getCalculatedOpenAPI();
160160
Components components = openAPI.getComponents();
161161
Paths paths = openAPI.getPaths();
@@ -176,7 +176,7 @@ protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
176176
RequestMapping reqMappingClass = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(),
177177
RequestMapping.class);
178178

179-
MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType());
179+
MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces);
180180
methodAttributes.setMethodOverloaded(existingOperation != null);
181181

182182
if (reqMappingClass != null) {
@@ -234,6 +234,24 @@ protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
234234
}
235235
}
236236

237+
protected void calculatePath(String operationPath, Set<RequestMethod> requestMethods, io.swagger.v3.oas.annotations.Operation apiOperation, String[] methodConsumes, String[] methodProduces) {
238+
OpenAPI openAPI = openAPIBuilder.getCalculatedOpenAPI();
239+
Paths paths = openAPI.getPaths();
240+
241+
for (RequestMethod requestMethod : requestMethods) {
242+
MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces);
243+
Operation operation = new Operation();
244+
openAPI = operationParser.parse(apiOperation, operation, openAPI, methodAttributes);
245+
PathItem pathItemObject = buildPathItem(requestMethod, operation, operationPath, paths);
246+
paths.addPathItem(operationPath, pathItemObject);
247+
}
248+
}
249+
250+
protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
251+
Set<RequestMethod> requestMethods){
252+
this.calculatePath(handlerMethod, operationPath,requestMethods,null, null);
253+
}
254+
237255
private void calculateJsonView(io.swagger.v3.oas.annotations.Operation apiOperation,
238256
MethodAttributes methodAttributes, Method method) {
239257
JsonView jsonViewAnnotation;

springdoc-openapi-common/src/main/java/org/springdoc/core/MethodAttributes.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ public MethodAttributes(String defaultConsumesMediaType, String defaultProducesM
7474
this.headers = new LinkedHashMap<>();
7575
}
7676

77+
public MethodAttributes(String defaultConsumesMediaType, String defaultProducesMediaType, String[] methodConsumes, String[] methodProduces) {
78+
this.defaultConsumesMediaType = defaultConsumesMediaType;
79+
this.defaultProducesMediaType = defaultProducesMediaType;
80+
this.methodProduces = methodProduces;
81+
this.methodConsumes = methodConsumes;
82+
this.headers = new LinkedHashMap<>();
83+
}
84+
7785
public String[] getClassProduces() {
7886
return classProduces;
7987
}
@@ -133,7 +141,7 @@ else if (reqMappingClass != null) {
133141
fillMethods(reqMappingClass.produces(), reqMappingClass.consumes(), reqMappingClass.headers());
134142
}
135143
else
136-
fillMethods(null, null, null);
144+
fillMethods(methodProduces, methodConsumes, null);
137145
}
138146

139147
private void fillMethods(String[] produces, String[] consumes, String[] headers) {
@@ -207,4 +215,5 @@ public ApiResponses calculateGenericMapResponse(Map<String, ApiResponse> generic
207215
public Map<String, ApiResponse> getGenericMapResponse() {
208216
return genericMapResponse;
209217
}
218+
210219
}

springdoc-openapi-webflux-core/src/main/java/org/springdoc/webflux/annotations/RouterOperation.java

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
import java.lang.annotation.RetentionPolicy;
2626
import java.lang.annotation.Target;
2727

28-
import org.springframework.web.bind.annotation.RequestMapping;
28+
import io.swagger.v3.oas.annotations.Operation;
29+
2930
import org.springframework.web.bind.annotation.RequestMethod;
3031

3132
/**
@@ -38,47 +39,69 @@
3839
@Retention(RetentionPolicy.RUNTIME)
3940
@Inherited
4041
public @interface RouterOperation {
41-
42-
4342
/**
44-
* Alias for {@link RequestMapping#path}.
43+
* The path mapping URIs (e.g. {@code "/profile"}).
44+
* Path mapping URIs may contain placeholders (e.g. <code>"/${profile_path}"</code>).
4545
*/
4646
String path();
4747

4848
/**
49-
* Alias for {@link RequestMapping#method}.
49+
* The HTTP request methods to map to, narrowing the primary mapping:
50+
* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
5051
*/
5152
RequestMethod[] method();
5253

5354
/**
54-
* Alias for {@link RequestMapping#consumes}.
55+
* Narrows the primary mapping by media types that can be consumed by the
56+
* mapped handler. Consists of one or more media types one of which must
57+
* match to the request {@code Content-Type} header. Examples:
58+
* <pre class="code">
59+
* consumes = "text/plain"
60+
* consumes = {"text/plain", "application/*"}
61+
* consumes = MediaType.TEXT_PLAIN_VALUE
62+
* </pre>
5563
*/
5664
String[] consumes() default {};
5765

5866
/**
59-
* Alias for {@link RequestMapping#produces}.
67+
* Narrows the primary mapping by media types that can be produced by the
68+
* mapped handler. Consists of one or more media types one of which must
69+
* be chosen via content negotiation against the "acceptable" media types
70+
* of the request. Typically those are extracted from the {@code "Accept"}
71+
* header but may be derived from query parameters, or other. Examples:
72+
* <pre class="code">
73+
* produces = "text/plain"
74+
* produces = {"text/plain", "application/*"}
75+
* produces = MediaType.TEXT_PLAIN_VALUE
76+
* produces = "text/plain;charset=UTF-8"
77+
* </pre>
6078
*/
6179
String[] produces() default {};
6280

6381
/**
6482
* The class of the Handler bean.
65-
*
6683
* @return the class of the Bean
6784
**/
6885
Class<?> beanClass() default Void.class;
6986

7087
/**
7188
* The method of the handler Bean.
72-
*
7389
* @return The method of the handler Bean.
7490
**/
7591
String beanMethod() default "";
7692

7793
/**
7894
* The parameters of the handler method.
79-
*
8095
* @return The parameters of the handler method.
8196
**/
8297
Class<?>[] parameterTypes() default {};
8398

99+
/**
100+
* The swagger operation description
101+
* Alias for {@link Operation}.
102+
* @return The operation
103+
**/
104+
Operation operation() default @Operation();
105+
106+
84107
}

springdoc-openapi-webflux-core/src/main/java/org/springdoc/webflux/api/OpenApiResource.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,12 @@ private void getRouterFunctionPaths() {
184184
LOGGER.error(e.getMessage());
185185
}
186186
if (handlerMethod != null && isPackageToScan(handlerMethod.getBeanType().getPackage().getName()) && isPathToMatch(routerOperation.path()))
187-
calculatePath(handlerMethod, routerOperation.path(), new HashSet<>(Arrays.asList(routerOperation.method())));
187+
calculatePath(handlerMethod, routerOperation.path(), new HashSet<>(Arrays.asList(routerOperation.method())), routerOperation.consumes(), routerOperation.produces());
188188
}
189189
}
190+
else if (StringUtils.isNotBlank(routerOperation.operation().operationId())) {
191+
calculatePath(routerOperation.path(), new HashSet<>(Arrays.asList(routerOperation.method())), routerOperation.operation(), routerOperation.consumes(), routerOperation.produces());
192+
}
190193
}
191194
}
192195
}

springdoc-openapi-webflux-core/src/test/java/test/org/springdoc/api/app71/EmployeeFunctionalConfig.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
package test.org.springdoc.api.app71;
22

3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.Parameter;
5+
import io.swagger.v3.oas.annotations.enums.ParameterIn;
6+
import io.swagger.v3.oas.annotations.media.Content;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
39
import org.springdoc.webflux.annotations.RouterOperation;
410
import org.springdoc.webflux.annotations.RouterOperations;
511

612
import org.springframework.context.annotation.Bean;
713
import org.springframework.context.annotation.Configuration;
14+
import org.springframework.http.MediaType;
815
import org.springframework.web.bind.annotation.RequestMethod;
916
import org.springframework.web.reactive.function.BodyExtractors;
1017
import org.springframework.web.reactive.function.server.RouterFunction;
1118
import org.springframework.web.reactive.function.server.ServerResponse;
1219

1320
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
1421
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
22+
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
1523
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
1624
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
1725

@@ -25,25 +33,31 @@ EmployeeRepository employeeRepository() {
2533
}
2634

2735
@Bean
28-
@RouterOperation(path = "/employees", method = RequestMethod.GET, beanClass = EmployeeRepository.class, beanMethod = "findAllEmployees")
36+
@RouterOperation(path = "/employees", method = RequestMethod.GET, beanClass = EmployeeRepository.class, beanMethod = "findAllEmployees", consumes = MediaType.APPLICATION_JSON_VALUE)
2937
RouterFunction<ServerResponse> getAllEmployeesRoute() {
30-
return route(GET("/employees"),
38+
return route(GET("/employees").and(accept(MediaType.APPLICATION_JSON)),
3139
req -> ok().body(
3240
employeeRepository().findAllEmployees(), Employee.class));
3341
}
3442

3543
@Bean
36-
@RouterOperation(path = "/employees/{id}", method = RequestMethod.GET, beanClass = EmployeeRepository.class, beanMethod = "findEmployeeById")
44+
@RouterOperation(path = "/employees/{id}", method = RequestMethod.GET, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE,
45+
operation = @Operation(operationId = "findEmployeeById", summary = "Find purchase order by ID", tags = { "MyEmployee" },
46+
parameters = { @Parameter(in = ParameterIn.PATH, name = "id", description = "Employee Id", schema = @Schema(type = "string")) },
47+
responses = { @ApiResponse(responseCode = "200", description = "successful operation", content = @Content(schema = @Schema(implementation = Employee.class))),
48+
@ApiResponse(responseCode = "400", description = "Invalid Employee ID supplied"),
49+
@ApiResponse(responseCode = "404", description = "Employee not found") }))
3750
RouterFunction<ServerResponse> getEmployeeByIdRoute() {
3851
return route(GET("/employees/{id}"),
3952
req -> ok().body(
4053
employeeRepository().findEmployeeById(req.pathVariable("id")), Employee.class));
4154
}
4255

56+
4357
@Bean
44-
@RouterOperation(path = "/employees/update", method = RequestMethod.POST, beanClass = EmployeeRepository.class, beanMethod = "updateEmployee")
58+
@RouterOperation(path = "/employees/update", method = RequestMethod.POST, beanClass = EmployeeRepository.class, beanMethod = "updateEmployee", consumes = MediaType.APPLICATION_XML_VALUE)
4559
RouterFunction<ServerResponse> updateEmployeeRoute() {
46-
return route(POST("/employees/update"),
60+
return route(POST("/employees/update").and(accept(MediaType.APPLICATION_XML)),
4761
req -> req.body(BodyExtractors.toMono(Employee.class))
4862
.doOnNext(employeeRepository()::updateEmployee)
4963
.then(ok().build()));

springdoc-openapi-webflux-core/src/test/resources/results/app71.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@
3737
"/employees/{id}": {
3838
"get": {
3939
"tags": [
40-
"employee-repository"
40+
"MyEmployee"
4141
],
42+
"summary": "Find purchase order by ID",
4243
"operationId": "findEmployeeById",
4344
"parameters": [
4445
{
4546
"name": "id",
4647
"in": "path",
48+
"description": "Employee Id",
4749
"required": true,
4850
"schema": {
4951
"type": "string"
@@ -52,14 +54,20 @@
5254
],
5355
"responses": {
5456
"200": {
55-
"description": "OK",
57+
"description": "successful operation",
5658
"content": {
57-
"*/*": {
59+
"application/json": {
5860
"schema": {
5961
"$ref": "#/components/schemas/Employee"
6062
}
6163
}
6264
}
65+
},
66+
"400": {
67+
"description": "Invalid Employee ID supplied"
68+
},
69+
"404": {
70+
"description": "Employee not found"
6371
}
6472
}
6573
}
@@ -72,7 +80,7 @@
7280
"operationId": "updateEmployee",
7381
"requestBody": {
7482
"content": {
75-
"application/json": {
83+
"application/xml": {
7684
"schema": {
7785
"$ref": "#/components/schemas/Employee"
7886
}

0 commit comments

Comments
 (0)