Skip to content

Commit 456962a

Browse files
author
bnasslahsen
committed
Improve support of Webflux with Functional Endpoints
1 parent 5c784fa commit 456962a

File tree

12 files changed

+301
-152
lines changed

12 files changed

+301
-152
lines changed

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import org.springdoc.core.SpringDocConfigProperties.GroupConfig;
5959
import org.springdoc.core.customizers.OpenApiCustomiser;
6060
import org.springdoc.core.customizers.OperationCustomizer;
61+
import org.springdoc.core.models.RouterFunctionData;
6162
import org.springdoc.core.models.RouterOperation;
6263

6364
import org.springframework.context.ApplicationContext;
@@ -461,4 +462,35 @@ protected Operation customiseOperation(Operation operation, HandlerMethod handle
461462
return operation;
462463
}
463464

465+
protected void merge(List<RouterFunctionData> routerFunctionDatas, List<org.springdoc.core.models.RouterOperation> routerOperationList) {
466+
for (org.springdoc.core.models.RouterOperation routerOperation : routerOperationList) {
467+
List<RouterFunctionData> routerFunctionDataList = routerFunctionDatas.stream()
468+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath()))
469+
.collect(Collectors.toList());
470+
if (!CollectionUtils.isEmpty(routerFunctionDataList)) {
471+
//Try with unique path in the route
472+
if (routerFunctionDataList.size() == 1)
473+
fillRouterOperation(routerFunctionDataList, routerOperation);
474+
//Try with unique path and RequestMethod
475+
else {
476+
routerFunctionDataList = routerFunctionDatas.stream()
477+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath()) && ArrayUtils.isNotEmpty(routerOperation.getMethod()) && routerFunctionData1.getMethods()[0].equals(routerOperation.getMethod()[0]))
478+
.collect(Collectors.toList());
479+
if (routerFunctionDataList.size() == 1)
480+
fillRouterOperation(routerFunctionDataList, routerOperation);
481+
}
482+
}
483+
}
484+
}
485+
486+
private void fillRouterOperation(List<RouterFunctionData> routerFunctionDataList, org.springdoc.core.models.RouterOperation routerOperation) {
487+
RouterFunctionData routerFunctionData = routerFunctionDataList.get(0);
488+
if (ArrayUtils.isEmpty(routerOperation.getConsumes()))
489+
routerOperation.setConsumes(routerFunctionData.getConsumes());
490+
if (ArrayUtils.isEmpty(routerOperation.getHeaders()))
491+
routerOperation.setHeaders(routerFunctionData.getHeaders());
492+
if (ArrayUtils.isEmpty(routerOperation.getMethod()))
493+
routerOperation.setMethod(routerFunctionData.getMethods());
494+
}
495+
464496
}

springdoc-openapi-common/src/main/java/org/springdoc/core/models/RouterFunctionData.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,7 @@ public class RouterFunctionData {
2020

2121
private RequestMethod[] methods;
2222

23-
public RouterFunctionData(String path, String consumes, String header, String queryParam, Set<HttpMethod> methods) {
24-
this.path = path;
25-
this.queryParam = queryParam;
26-
this.methods = getMethod(methods);
27-
if (StringUtils.isNotBlank(header))
28-
this.headers = new String[] { header };
29-
if (StringUtils.isNotBlank(consumes))
30-
this.consumes = new String[] { consumes };
23+
public RouterFunctionData() {
3124
}
3225

3326
public String getPath() {
@@ -54,6 +47,11 @@ public void setHeaders(String[] headers) {
5447
this.headers = headers;
5548
}
5649

50+
public void setHeaders(String headers) {
51+
if (StringUtils.isNotBlank(headers))
52+
this.headers = new String[] { headers };
53+
}
54+
5755
public RequestMethod[] getMethods() {
5856
return methods;
5957
}
@@ -62,6 +60,10 @@ public void setMethods(RequestMethod[] methods) {
6260
this.methods = methods;
6361
}
6462

63+
public void setMethods(Set<HttpMethod> methods) {
64+
this.methods = getMethod(methods);
65+
}
66+
6567
public String[] getConsumes() {
6668
return consumes;
6769
}
@@ -70,6 +72,10 @@ public void setConsumes(String[] consumes) {
7072
this.consumes = consumes;
7173
}
7274

75+
public void setConsumes(String consumes) {
76+
if (StringUtils.isNotBlank(consumes))
77+
this.consumes = new String[] { consumes };
78+
}
7379
private RequestMethod[] getMethod(Set<HttpMethod> methods) {
7480
if (!CollectionUtils.isEmpty(methods)) {
7581
return methods.stream().map(httpMethod -> getRequestMethod(httpMethod)).toArray(RequestMethod[]::new);

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.swagger.v3.core.util.Yaml;
3434
import io.swagger.v3.oas.annotations.Operation;
3535
import io.swagger.v3.oas.models.OpenAPI;
36+
import org.apache.commons.lang3.ArrayUtils;
3637
import org.springdoc.api.AbstractOpenApiResource;
3738
import org.springdoc.core.AbstractRequestBuilder;
3839
import org.springdoc.core.GenericResponseBuilder;
@@ -43,13 +44,15 @@
4344
import org.springdoc.core.annotations.RouterOperations;
4445
import org.springdoc.core.customizers.OpenApiCustomiser;
4546
import org.springdoc.core.customizers.OperationCustomizer;
47+
import org.springdoc.core.models.RouterFunctionData;
4648
import reactor.core.publisher.Mono;
4749

4850
import org.springframework.beans.factory.annotation.Autowired;
4951
import org.springframework.beans.factory.annotation.Value;
5052
import org.springframework.context.ApplicationContext;
5153
import org.springframework.http.MediaType;
5254
import org.springframework.http.server.reactive.ServerHttpRequest;
55+
import org.springframework.util.CollectionUtils;
5356
import org.springframework.web.bind.annotation.GetMapping;
5457
import org.springframework.web.bind.annotation.RequestMethod;
5558
import org.springframework.web.bind.annotation.RestController;
@@ -144,7 +147,6 @@ private void getRouterFunctionPaths() {
144147
RouterFunction routerFunction = entry.getValue();
145148
RouterFunctionVisitor routerFunctionVisitor = new RouterFunctionVisitor();
146149
routerFunction.accept(routerFunctionVisitor);
147-
148150
List<RouterOperation> routerOperationList = new ArrayList<>();
149151
RouterOperations routerOperations = applicationContext.findAnnotationOnBean(entry.getKey(), RouterOperations.class);
150152
if (routerOperations == null) {
@@ -154,10 +156,12 @@ private void getRouterFunctionPaths() {
154156
else
155157
routerOperationList.addAll(Arrays.asList(routerOperations.value()));
156158
if (routerOperationList.size() == 1)
157-
calculatePath(routerOperationList.stream().map(routerOperation -> new org.springdoc.core.models.RouterOperation(routerOperation, routerFunctionVisitor.getRouterFunctionData())).collect(Collectors.toList()));
158-
else
159-
calculatePath(routerOperationList.stream().map(routerOperation -> new org.springdoc.core.models.RouterOperation(routerOperation)).collect(Collectors.toList()));
160-
//TODO
159+
calculatePath(routerOperationList.stream().map(routerOperation -> new org.springdoc.core.models.RouterOperation(routerOperation, routerFunctionVisitor.getRouterFunctionDatas().get(0))).collect(Collectors.toList()));
160+
else {
161+
List<org.springdoc.core.models.RouterOperation> operationList = routerOperationList.stream().map(routerOperation -> new org.springdoc.core.models.RouterOperation(routerOperation)).collect(Collectors.toList());
162+
merge(routerFunctionVisitor.getRouterFunctionDatas(), operationList);
163+
calculatePath(operationList);
164+
}
161165
}
162166
}
163167

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.springdoc.webflux.api;
22

3+
import java.util.ArrayList;
4+
import java.util.List;
35
import java.util.Set;
46
import java.util.function.Function;
57

@@ -18,78 +20,69 @@
1820

1921
public class RouterFunctionVisitor implements RouterFunctions.Visitor, RequestPredicates.Visitor {
2022

21-
private Set<HttpMethod> methods;
23+
private List<RouterFunctionData> routerFunctionDatas = new ArrayList<>();
2224

23-
private String path;
24-
25-
private String consumes;
26-
27-
private String header;
28-
29-
private String queryParam;
25+
private RouterFunctionData routerFunctionData;
3026

3127
private RequestPredicate routePredicate;
3228

3329
private HandlerFunction<?> routeHandlerFunction;
3430

35-
private RequestPredicate unknownPredicate;
36-
37-
private Function<ServerRequest, Mono<Resource>> lookupFunction;
38-
39-
private RouterFunction unknownRouterFunction;
40-
4131
@Override
42-
public void startNested(RequestPredicate predicate) {
32+
public void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction) {
33+
this.routePredicate = predicate;
34+
this.routeHandlerFunction = handlerFunction;
35+
this.routerFunctionData = new RouterFunctionData();
36+
routerFunctionDatas.add(this.routerFunctionData);
37+
predicate.accept(this);
4338
}
4439

4540
@Override
46-
public void endNested(RequestPredicate predicate) {
41+
public void method(Set<HttpMethod> methods) {
42+
routerFunctionData.setMethods(methods);
4743
}
4844

4945
@Override
50-
public void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction) {
51-
this.routePredicate = predicate;
52-
this.routeHandlerFunction = handlerFunction;
53-
predicate.accept(this);
46+
public void path(String pattern) {
47+
routerFunctionData.setPath(pattern);
5448
}
5549

5650
@Override
57-
public void resources(Function<ServerRequest, Mono<Resource>> lookupFunction) {
58-
this.lookupFunction = lookupFunction;
51+
public void header(String name, String value) {
52+
if (HttpHeaders.ACCEPT.equals(name))
53+
routerFunctionData.setConsumes(value);
54+
else
55+
routerFunctionData.setHeaders(name + "=" + value);
5956
}
6057

61-
@Override
62-
public void unknown(RouterFunction<?> routerFunction) {
63-
this.unknownRouterFunction = routerFunction;
58+
public List<RouterFunctionData> getRouterFunctionDatas() {
59+
return routerFunctionDatas;
6460
}
6561

66-
// RequestPredicates.Visitor
6762

6863
@Override
69-
public void method(Set<HttpMethod> methods) {
70-
this.methods = methods;
64+
public void startNested(RequestPredicate predicate) {
7165
}
7266

7367
@Override
74-
public void path(String pattern) {
75-
this.path = pattern;
68+
public void endNested(RequestPredicate predicate) {
7669
}
7770

7871
@Override
79-
public void pathExtension(String extension) {
72+
public void resources(Function<ServerRequest, Mono<Resource>> lookupFunction) {
8073
}
8174

8275
@Override
83-
public void header(String name, String value) {
84-
if (HttpHeaders.ACCEPT.equals(name))
85-
this.consumes = value;
86-
else
87-
this.header = name + "=" + value;
76+
public void unknown(RouterFunction<?> routerFunction) {
77+
}
78+
79+
@Override
80+
public void pathExtension(String extension) {
8881
}
8982

83+
9084
@Override
9185
public void queryParam(String name, String value) {
92-
this.queryParam = name + "=" + value;
9386
}
9487

9588
@Override
@@ -98,7 +91,6 @@ public void startAnd() {
9891

9992
@Override
10093
public void and() {
101-
//TODO
10294
}
10395

10496
@Override
@@ -127,12 +119,8 @@ public void endNegate() {
127119

128120
@Override
129121
public void unknown(RequestPredicate predicate) {
130-
this.unknownPredicate = predicate;
131122
}
132123

133-
public RouterFunctionData getRouterFunctionData() {
134-
return new RouterFunctionData(this.path, this.consumes, this.header, this.queryParam, this.methods);
135-
}
136124
}
137125

138126

springdoc-openapi-webflux-core/src/test/java/test/org/springdoc/api/app69/RoutingConfiguration.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import org.springframework.context.annotation.Bean;
77
import org.springframework.context.annotation.Configuration;
88
import org.springframework.http.MediaType;
9-
import org.springframework.web.bind.annotation.RequestMethod;
109
import org.springframework.web.reactive.function.server.RouterFunction;
1110
import org.springframework.web.reactive.function.server.ServerResponse;
1211

@@ -21,12 +20,11 @@
2120
public class RoutingConfiguration {
2221

2322
@Bean
24-
@RouterOperations({ @RouterOperation(path = "/api/user/index", method = RequestMethod.GET, beanClass = UserRepository.class, beanMethod = "getAllUsers", consumes = MediaType.APPLICATION_JSON_VALUE),
25-
@RouterOperation(path = "/api/user/byFirstName", method = RequestMethod.GET, beanClass = UserRepository.class, beanMethod = "getAllUsers", parameterTypes = {String.class} , consumes = MediaType.APPLICATION_JSON_VALUE),
26-
@RouterOperation(path = "/api/user/{id}", method = RequestMethod.GET, beanClass = UserRepository.class, beanMethod = "getUserById", consumes = MediaType.APPLICATION_JSON_VALUE),
27-
@RouterOperation(path = "/api/user/post", method = RequestMethod.POST, beanClass = UserRepository.class, beanMethod = "saveUser", consumes = MediaType.APPLICATION_JSON_VALUE),
28-
@RouterOperation(path = "/api/user/put/{id}", method = RequestMethod.PUT, beanClass = UserRepository.class, beanMethod = "putUser", consumes = MediaType.APPLICATION_JSON_VALUE),
29-
@RouterOperation(path = "/api/user/delete/{id}", method = RequestMethod.DELETE, beanClass = UserRepository.class, beanMethod = "deleteUser", consumes = MediaType.APPLICATION_JSON_VALUE) })
23+
@RouterOperations({ @RouterOperation(path = "/api/user/index", beanClass = UserRepository.class, beanMethod = "getAllUsers"),
24+
@RouterOperation(path = "/api/user/{id}", beanClass = UserRepository.class, beanMethod = "getUserById"),
25+
@RouterOperation(path = "/api/user/post", beanClass = UserRepository.class, beanMethod = "saveUser"),
26+
@RouterOperation(path = "/api/user/put/{id}", beanClass = UserRepository.class, beanMethod = "putUser"),
27+
@RouterOperation(path = "/api/user/delete/{id}", beanClass = UserRepository.class, beanMethod = "deleteUser") })
3028
public RouterFunction<ServerResponse> monoRouterFunction(UserHandler userHandler) {
3129
return route(GET("/api/user/index").and(accept(MediaType.APPLICATION_JSON)), userHandler::getAll)
3230
.andRoute(GET("/api/user/{id}").and(accept(MediaType.APPLICATION_JSON)), userHandler::getUser)

springdoc-openapi-webflux-core/src/test/java/test/org/springdoc/api/app70/RouteConfig.java

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import org.springframework.context.annotation.Bean;
88
import org.springframework.context.annotation.Configuration;
99
import org.springframework.http.MediaType;
10-
import org.springframework.web.bind.annotation.RequestMethod;
1110
import org.springframework.web.reactive.function.server.RouterFunction;
1211
import org.springframework.web.reactive.function.server.ServerRequest;
1312
import org.springframework.web.reactive.function.server.ServerResponse;
@@ -17,37 +16,35 @@
1716

1817
@Configuration
1918
public class RouteConfig {
20-
private final CoffeeService service;
21-
22-
public RouteConfig(CoffeeService service) {
23-
this.service = service;
24-
}
25-
19+
private final CoffeeService service;
2620

21+
public RouteConfig(CoffeeService service) {
22+
this.service = service;
23+
}
2724

2825
@Bean
29-
@RouterOperations({ @RouterOperation(path = "/coffees", method = RequestMethod.GET, beanClass = CoffeeService.class, beanMethod = "getAllCoffees"),
30-
@RouterOperation(path = "/coffees/{id}", method = RequestMethod.GET, beanClass = CoffeeService.class, beanMethod = "getCoffeeById"),
31-
@RouterOperation(path = "/coffees/{id}/orders", method = RequestMethod.GET, beanClass = CoffeeService.class, beanMethod = "getOrdersForCoffeeById") })
32-
RouterFunction<ServerResponse> routerFunction() {
33-
return route(GET("/coffees"), this::all)
34-
.andRoute(GET("/coffees/{id}"), this::byId)
35-
.andRoute(GET("/coffees/{id}/orders"), this::orders);
36-
}
37-
38-
private Mono<ServerResponse> all(ServerRequest req) {
39-
return ServerResponse.ok()
40-
.body(service.getAllCoffees(), Coffee.class);
41-
}
42-
43-
private Mono<ServerResponse> byId(ServerRequest req) {
44-
return ServerResponse.ok()
45-
.body(service.getCoffeeById(req.pathVariable("id")), Coffee.class);
46-
}
47-
48-
private Mono<ServerResponse> orders(ServerRequest req) {
49-
return ServerResponse.ok()
50-
.contentType(MediaType.TEXT_EVENT_STREAM)
51-
.body(service.getOrdersForCoffeeById(req.pathVariable("id")), CoffeeOrder.class);
52-
}
26+
@RouterOperations({ @RouterOperation(path = "/coffees", beanClass = CoffeeService.class, beanMethod = "getAllCoffees"),
27+
@RouterOperation(path = "/coffees/{id}", beanClass = CoffeeService.class, beanMethod = "getCoffeeById"),
28+
@RouterOperation(path = "/coffees/{id}/orders", beanClass = CoffeeService.class, beanMethod = "getOrdersForCoffeeById") })
29+
RouterFunction<ServerResponse> routerFunction() {
30+
return route(GET("/coffees"), this::all)
31+
.andRoute(GET("/coffees/{id}"), this::byId)
32+
.andRoute(GET("/coffees/{id}/orders"), this::orders);
33+
}
34+
35+
private Mono<ServerResponse> all(ServerRequest req) {
36+
return ServerResponse.ok()
37+
.body(service.getAllCoffees(), Coffee.class);
38+
}
39+
40+
private Mono<ServerResponse> byId(ServerRequest req) {
41+
return ServerResponse.ok()
42+
.body(service.getCoffeeById(req.pathVariable("id")), Coffee.class);
43+
}
44+
45+
private Mono<ServerResponse> orders(ServerRequest req) {
46+
return ServerResponse.ok()
47+
.contentType(MediaType.TEXT_EVENT_STREAM)
48+
.body(service.getOrdersForCoffeeById(req.pathVariable("id")), CoffeeOrder.class);
49+
}
5350
}

0 commit comments

Comments
 (0)