Skip to content

Commit a7fbaa3

Browse files
committed
Add support for Spring Framework API Versioning with Functional Endpoints. Fixes #3229
1 parent be23b85 commit a7fbaa3

File tree

112 files changed

+5781
-128
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

112 files changed

+5781
-128
lines changed

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
import org.springdoc.core.providers.JavadocProvider;
106106
import org.springdoc.core.providers.ObjectMapperProvider;
107107
import org.springdoc.core.providers.SpringDocProviders;
108+
import org.springdoc.core.providers.SpringWebProvider;
108109
import org.springdoc.core.service.AbstractRequestService;
109110
import org.springdoc.core.service.GenericParameterService;
110111
import org.springdoc.core.service.GenericResponseService;
@@ -585,6 +586,13 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
585586
String[] headers = routerOperation.getHeaders();
586587
Map<String, String> queryParams = routerOperation.getQueryParams();
587588
SpringDocVersionStrategy springDocVersionStrategy = routerOperation.getSpringDocVersionStrategy();
589+
if (springDocVersionStrategy == null && routerOperation.getVersion() != null) {
590+
Optional<SpringWebProvider> springWebProviderOpt = springDocProviders.getSpringWebProvider();
591+
if (springWebProviderOpt.isPresent()) {
592+
springDocVersionStrategy = springWebProviderOpt.get().getSpringDocVersionStrategy(
593+
routerOperation.getVersion(), routerOperation.getParams());
594+
}
595+
}
588596
if (springDocVersionStrategy != null)
589597
queryParams = springDocVersionStrategy.updateQueryParams(queryParams);
590598
Components components = openAPI.getComponents();
@@ -880,14 +888,12 @@ protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVis
880888
if (routerOperationList.size() == 1) {
881889
List<RouterFunctionData> datas = routerFunctionVisitor.getRouterFunctionDatas();
882890
List<RouterOperation> operationList = routerOperationList.stream().map(routerOperation -> new RouterOperation(routerOperation, datas.get(0))).collect(Collectors.toList());
883-
resolveRouterFunctionVersionStrategies(datas, operationList);
884891
calculatePath(operationList, locale, openAPI);
885892
}
886893
else {
887894
List<RouterFunctionData> datas = routerFunctionVisitor.getRouterFunctionDatas();
888895
List<RouterOperation> operationList = routerOperationList.stream().map(RouterOperation::new).collect(Collectors.toList());
889896
mergeRouters(datas, operationList);
890-
resolveRouterFunctionVersionStrategies(datas, operationList);
891897
calculatePath(operationList, locale, openAPI);
892898
}
893899
}
@@ -907,13 +913,9 @@ private void resolveRouterFunctionVersionStrategies(List<RouterFunctionData> dat
907913
RouterOperation op = operations.get(i);
908914
SpringDocVersionStrategy strategy = springWebProvider.getSpringDocVersionStrategy(version, datas.get(i).getParams());
909915
if (strategy != null) {
910-
// Ensure version is set for functional routes where params may be empty
911-
if (strategy.getVersion() == null) {
912-
strategy.setVersion(version);
913-
}
914916
op.setPath(strategy.updateOperationPath(op.getPath(), version));
915917
}
916-
op.setVersionStrategy(strategy);
918+
op.setVersion(version);
917919
}
918920
}
919921
});

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/fn/RouterOperation.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ public class RouterOperation implements Comparable<RouterOperation> {
113113
*/
114114
private SpringDocVersionStrategy springDocVersionStrategy;
115115

116+
/**
117+
* The Version.
118+
*/
119+
private String version;
120+
116121
/**
117122
* Instantiates a new Router operation.
118123
*/
@@ -155,6 +160,7 @@ public RouterOperation(org.springdoc.core.annotations.RouterOperation routerOper
155160
this.headers = ArrayUtils.isEmpty(routerOperationAnnotation.headers()) ? routerFunctionData.getHeaders() : routerOperationAnnotation.headers();
156161
this.params = routerOperationAnnotation.params();
157162
this.queryParams = routerFunctionData.getQueryParams();
163+
this.version = routerFunctionData.getVersion();
158164
}
159165

160166
/**
@@ -191,6 +197,7 @@ public RouterOperation(RouterFunctionData routerFunctionData) {
191197
this.headers = routerFunctionData.getHeaders();
192198
this.params = routerFunctionData.getParams();
193199
this.queryParams = routerFunctionData.getQueryParams();
200+
this.version = routerFunctionData.getVersion();
194201

195202
Map<String, Object> attributes = routerFunctionData.getAttributes();
196203
if (attributes.containsKey(OPERATION_ATTRIBUTE)) {
@@ -448,6 +455,24 @@ public void setVersionStrategy(SpringDocVersionStrategy springDocVersionStrategy
448455
this.springDocVersionStrategy = springDocVersionStrategy;
449456
}
450457

458+
/**
459+
* Gets version.
460+
*
461+
* @return the version
462+
*/
463+
public String getVersion() {
464+
return version;
465+
}
466+
467+
/**
468+
* Sets version.
469+
*
470+
* @param version the version
471+
*/
472+
public void setVersion(String version) {
473+
this.version = version;
474+
}
475+
451476
@Override
452477
public int compareTo(RouterOperation routerOperation) {
453478
int result = path.compareTo(routerOperation.getPath());

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/versions/QueryParamVersionStrategy.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.LinkedHashMap;
2929
import java.util.Map;
3030

31+
import org.apache.commons.lang3.ArrayUtils;
32+
3133
/**
3234
* The type Query param version strategy.
3335
*
@@ -64,6 +66,10 @@ public String getParameterName() {
6466

6567
@Override
6668
public void updateVersion(String version, String[] params) {
69+
if(ArrayUtils.isEmpty(params)) {
70+
setVersion(version);
71+
return;
72+
}
6773
for (String param : params) {
6874
if (param.contains("=")) {
6975
String[] paramValues = param.split("=", 2);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2025 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package test.org.springdoc.api.v31.app199;
26+
27+
import test.org.springdoc.api.v31.AbstractSpringDocTest;
28+
29+
import org.springframework.boot.autoconfigure.SpringBootApplication;
30+
import org.springframework.context.annotation.ComponentScan;
31+
32+
/**
33+
* Tests media-type versioning support for functional router endpoints.
34+
*
35+
* @author bnasslahsen
36+
*/
37+
public class SpringDocApp199Test extends AbstractSpringDocTest {
38+
39+
@SpringBootApplication
40+
@ComponentScan(basePackages = { "org.springdoc", "test.org.springdoc.api.v31.app199" })
41+
static class SpringDocTestApp {
42+
43+
}
44+
45+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2025 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package test.org.springdoc.api.v31.app199;
26+
27+
import reactor.core.publisher.Mono;
28+
29+
import org.springframework.stereotype.Component;
30+
import org.springframework.web.reactive.function.server.ServerRequest;
31+
import org.springframework.web.reactive.function.server.ServerResponse;
32+
33+
/**
34+
* The type User handler.
35+
*
36+
* @author bnasslahsen
37+
*/
38+
@Component
39+
public class UserHandler {
40+
41+
private final UserService userService;
42+
43+
/**
44+
* Instantiates a new User handler.
45+
* @param userService the user service
46+
*/
47+
public UserHandler(UserService userService) {
48+
this.userService = userService;
49+
}
50+
51+
/**
52+
* Get users media v1.
53+
* @param request the request
54+
* @return the server response
55+
*/
56+
public Mono<ServerResponse> getUsersMediaV1(ServerRequest request) {
57+
return ServerResponse.ok().bodyValue(userService.getUsersMediaV1());
58+
}
59+
60+
/**
61+
* Get users media v2.
62+
* @param request the request
63+
* @return the server response
64+
*/
65+
public Mono<ServerResponse> getUsersMediaV2(ServerRequest request) {
66+
return ServerResponse.ok().bodyValue(userService.getUsersMediaV2());
67+
}
68+
69+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2025 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package test.org.springdoc.api.v31.app199;
26+
27+
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
28+
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.context.annotation.Configuration;
31+
import org.springframework.http.MediaType;
32+
import org.springframework.web.reactive.function.server.RequestPredicates;
33+
import org.springframework.web.reactive.function.server.RouterFunction;
34+
import org.springframework.web.reactive.function.server.ServerResponse;
35+
36+
/**
37+
* The type User router config.
38+
*
39+
* @author bnasslahsen
40+
*/
41+
@Configuration
42+
public class UserRouterConfig {
43+
44+
/**
45+
* Routes router function.
46+
* @param handler the handler
47+
* @return the router function
48+
*/
49+
@Bean
50+
RouterFunction<ServerResponse> routes(UserHandler handler) {
51+
return SpringdocRouteBuilder.route()
52+
.GET("/api/users/media",
53+
RequestPredicates.version("1.0").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)),
54+
handler::getUsersMediaV1, ops -> ops.beanClass(UserService.class).beanMethod("getUsersMediaV1"))
55+
.GET("/api/users/media",
56+
RequestPredicates.version("2.0").and(RequestPredicates.accept(MediaType.APPLICATION_JSON)),
57+
handler::getUsersMediaV2, ops -> ops.beanClass(UserService.class).beanMethod("getUsersMediaV2"))
58+
.build();
59+
}
60+
61+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * *
6+
* * * * * Copyright 2019-2025 the original author or authors.
7+
* * * * *
8+
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
9+
* * * * * you may not use this file except in compliance with the License.
10+
* * * * * You may obtain a copy of the License at
11+
* * * * *
12+
* * * * * https://www.apache.org/licenses/LICENSE-2.0
13+
* * * * *
14+
* * * * * Unless required by applicable law or agreed to in writing, software
15+
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
16+
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* * * * * See the License for the specific language governing permissions and
18+
* * * * * limitations under the License.
19+
* * * *
20+
* * *
21+
* *
22+
*
23+
*/
24+
25+
package test.org.springdoc.api.v31.app199;
26+
27+
import java.util.List;
28+
import java.util.stream.Collectors;
29+
30+
import test.org.springdoc.api.v31.app199.user.UserDTOv1;
31+
import test.org.springdoc.api.v31.app199.user.UserDTOv2;
32+
import test.org.springdoc.api.v31.app199.user.UserMapper;
33+
import test.org.springdoc.api.v31.app199.user.UserRepository;
34+
35+
import org.springframework.stereotype.Service;
36+
37+
/**
38+
* The type User service. Provides typed return methods for springdoc introspection.
39+
*
40+
* @author bnasslahsen
41+
*/
42+
@Service
43+
public class UserService {
44+
45+
private final UserRepository userRepository;
46+
47+
private final UserMapper userMapper;
48+
49+
/**
50+
* Instantiates a new User service.
51+
* @param userRepository the user repository
52+
* @param userMapper the user mapper
53+
*/
54+
public UserService(UserRepository userRepository, UserMapper userMapper) {
55+
this.userRepository = userRepository;
56+
this.userMapper = userMapper;
57+
}
58+
59+
/**
60+
* Gets users media v1.
61+
* @return the users v1
62+
*/
63+
public List<UserDTOv1> getUsersMediaV1() {
64+
return userRepository.findAll().stream().map(userMapper::toV1).collect(Collectors.toList());
65+
}
66+
67+
/**
68+
* Gets users media v2.
69+
* @return the users v2
70+
*/
71+
public List<UserDTOv2> getUsersMediaV2() {
72+
return userRepository.findAll().stream().map(userMapper::toV2).collect(Collectors.toList());
73+
}
74+
75+
}

0 commit comments

Comments
 (0)