Skip to content

Commit 190f24f

Browse files
committed
fix for flaky operationIds
1 parent 05be6a8 commit 190f24f

File tree

10 files changed

+562
-23
lines changed

10 files changed

+562
-23
lines changed

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

+10-8
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,7 @@
2020

2121
package org.springdoc.webflux.api;
2222

23-
import java.util.HashSet;
24-
import java.util.LinkedHashMap;
25-
import java.util.List;
26-
import java.util.Map;
27-
import java.util.Objects;
28-
import java.util.Optional;
29-
import java.util.Set;
23+
import java.util.*;
3024

3125
import com.fasterxml.jackson.core.JsonProcessingException;
3226
import io.swagger.v3.core.util.Json;
@@ -190,7 +184,9 @@ protected void getPaths(Map<String, Object> restControllers) {
190184
* @param map the map
191185
*/
192186
protected void calculatePath(Map<String, Object> restControllers, Map<RequestMappingInfo, HandlerMethod> map) {
193-
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
187+
List<Map.Entry<RequestMappingInfo, HandlerMethod>> entries = new ArrayList<>(map.entrySet());
188+
entries.sort(byReversedRequestMappingInfos());
189+
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : entries) {
194190
RequestMappingInfo requestMappingInfo = entry.getKey();
195191
HandlerMethod handlerMethod = entry.getValue();
196192
PatternsRequestCondition patternsRequestCondition = requestMappingInfo.getPatternsCondition();
@@ -215,6 +211,12 @@ && isFilterCondition(handlerMethod, operationPath, produces, consumes, headers))
215211
}
216212
}
217213

214+
private Comparator<Map.Entry<RequestMappingInfo, HandlerMethod>> byReversedRequestMappingInfos() {
215+
return Comparator.<Map.Entry<RequestMappingInfo, HandlerMethod>, String>
216+
comparing(a -> a.getKey().toString())
217+
.reversed();
218+
}
219+
218220
/**
219221
* Gets web flux router function paths.
220222
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app81;
20+
21+
import org.springframework.web.bind.annotation.GetMapping;
22+
import org.springframework.web.bind.annotation.RequestParam;
23+
import org.springframework.web.bind.annotation.RestController;
24+
import reactor.core.publisher.Mono;
25+
26+
@RestController
27+
public class OperationIdController {
28+
29+
@GetMapping(path = "/test_0") // gets operationId opIdTest_3
30+
public Mono<String> opIdTest() {
31+
return null;
32+
}
33+
34+
@GetMapping(path = "/test_1") // gets operationId opIdTest_2
35+
public Mono<String> opIdTest(@RequestParam String param) {
36+
return null;
37+
}
38+
39+
@GetMapping(path = "/test_2") // gets operationId opIdTest_1
40+
public Mono<String> opIdTest(@RequestParam Integer param) {
41+
return null;
42+
}
43+
44+
@GetMapping(path = "/test_3") // gets operationId opIdTest
45+
public Mono<String> opIdTest(@RequestParam Boolean param) {
46+
return null;
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
*
3+
* *
4+
* * *
5+
* * * * Copyright 2019-2020 the original author or authors.
6+
* * * *
7+
* * * * Licensed under the Apache License, Version 2.0 (the "License");
8+
* * * * you may not use this file except in compliance with the License.
9+
* * * * You may obtain a copy of the License at
10+
* * * *
11+
* * * * https://www.apache.org/licenses/LICENSE-2.0
12+
* * * *
13+
* * * * Unless required by applicable law or agreed to in writing, software
14+
* * * * distributed under the License is distributed on an "AS IS" BASIS,
15+
* * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* * * * See the License for the specific language governing permissions and
17+
* * * * limitations under the License.
18+
* * *
19+
* *
20+
*
21+
*
22+
*/
23+
package test.org.springdoc.api.app81;
24+
25+
import org.junit.jupiter.api.RepeatedTest;
26+
import org.springdoc.webflux.api.OpenApiResource;
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.boot.autoconfigure.SpringBootApplication;
29+
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
30+
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
31+
import org.springframework.context.annotation.ComponentScan;
32+
import org.springframework.http.server.reactive.ServerHttpRequest;
33+
import org.springframework.test.context.ActiveProfiles;
34+
import org.springframework.test.context.TestPropertySource;
35+
import org.springframework.web.method.HandlerMethod;
36+
import org.springframework.web.reactive.result.method.RequestMappingInfo;
37+
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
38+
39+
import java.net.URI;
40+
import java.util.ArrayList;
41+
import java.util.Comparator;
42+
import java.util.List;
43+
import java.util.Map;
44+
import java.util.concurrent.ThreadLocalRandom;
45+
46+
import static org.mockito.Mockito.mock;
47+
import static org.mockito.Mockito.when;
48+
import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
49+
import static org.springdoc.core.Constants.SPRINGDOC_CACHE_DISABLED;
50+
import static test.org.springdoc.api.AbstractSpringDocTest.getContent;
51+
52+
53+
/**
54+
* Tests deterministic creation of operationIds
55+
*/
56+
@AutoConfigureWebTestClient(timeout = "3600000")
57+
@WebFluxTest(properties = SPRINGDOC_CACHE_DISABLED + "=true")
58+
@ActiveProfiles("test")
59+
public class SpringDocApp81Test {
60+
61+
@SpringBootApplication
62+
@ComponentScan(basePackages = {"org.springdoc", "test.org.springdoc.api.app81"})
63+
static class SpringDocTestApp {
64+
}
65+
66+
@Autowired
67+
OpenApiResource resource;
68+
69+
@Autowired
70+
RequestMappingInfoHandlerMapping mappingInfoHandlerMapping;
71+
72+
@RepeatedTest(10)
73+
public void shouldGenerateOperationIdsDeterministically() throws Exception {
74+
shuffleSpringHandlerMethods();
75+
76+
ServerHttpRequest request = mock(ServerHttpRequest.class);
77+
when(request.getURI()).thenReturn(URI.create("http://localhost"));
78+
79+
String expected = getContent("results/app81.json");
80+
String openApi = resource.openapiJson(request, "").block();
81+
assertEquals(expected, openApi, true);
82+
}
83+
84+
private void shuffleSpringHandlerMethods() {
85+
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mappingInfoHandlerMapping.getHandlerMethods();
86+
List<Map.Entry<RequestMappingInfo, HandlerMethod>> collect = new ArrayList<>(handlerMethods.entrySet());
87+
collect.sort(Comparator.comparing(a -> ThreadLocalRandom.current().nextBoolean() ? -1 : 1));
88+
89+
collect.forEach(e -> mappingInfoHandlerMapping.unregisterMapping(e.getKey()));
90+
collect.forEach(e -> mappingInfoHandlerMapping.registerMapping(e.getKey(), e.getValue().getBean(), e.getValue().getMethod()));
91+
}
92+
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
{
2+
"openapi": "3.0.1",
3+
"info": {
4+
"title": "OpenAPI definition",
5+
"version": "v0"
6+
},
7+
"servers": [
8+
{
9+
"url": "http://localhost",
10+
"description": "Generated server url"
11+
}
12+
],
13+
"paths": {
14+
"/test_3": {
15+
"get": {
16+
"tags": [
17+
"operation-id-controller"
18+
],
19+
"operationId": "opIdTest",
20+
"parameters": [
21+
{
22+
"name": "param",
23+
"in": "query",
24+
"required": true,
25+
"schema": {
26+
"type": "boolean"
27+
}
28+
}
29+
],
30+
"responses": {
31+
"200": {
32+
"description": "OK",
33+
"content": {
34+
"*/*": {
35+
"schema": {
36+
"type": "string"
37+
}
38+
}
39+
}
40+
}
41+
}
42+
}
43+
},
44+
"/test_2": {
45+
"get": {
46+
"tags": [
47+
"operation-id-controller"
48+
],
49+
"operationId": "opIdTest_1",
50+
"parameters": [
51+
{
52+
"name": "param",
53+
"in": "query",
54+
"required": true,
55+
"schema": {
56+
"type": "integer",
57+
"format": "int32"
58+
}
59+
}
60+
],
61+
"responses": {
62+
"200": {
63+
"description": "OK",
64+
"content": {
65+
"*/*": {
66+
"schema": {
67+
"type": "string"
68+
}
69+
}
70+
}
71+
}
72+
}
73+
}
74+
},
75+
"/test_1": {
76+
"get": {
77+
"tags": [
78+
"operation-id-controller"
79+
],
80+
"operationId": "opIdTest_2",
81+
"parameters": [
82+
{
83+
"name": "param",
84+
"in": "query",
85+
"required": true,
86+
"schema": {
87+
"type": "string"
88+
}
89+
}
90+
],
91+
"responses": {
92+
"200": {
93+
"description": "OK",
94+
"content": {
95+
"*/*": {
96+
"schema": {
97+
"type": "string"
98+
}
99+
}
100+
}
101+
}
102+
}
103+
}
104+
},
105+
"/test_0": {
106+
"get": {
107+
"tags": [
108+
"operation-id-controller"
109+
],
110+
"operationId": "opIdTest_3",
111+
"responses": {
112+
"200": {
113+
"description": "OK",
114+
"content": {
115+
"*/*": {
116+
"schema": {
117+
"type": "string"
118+
}
119+
}
120+
}
121+
}
122+
}
123+
}
124+
}
125+
},
126+
"components": {}
127+
}

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

+10-7
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@
2020

2121
package org.springdoc.webmvc.api;
2222

23-
import java.util.HashSet;
24-
import java.util.LinkedHashMap;
25-
import java.util.List;
26-
import java.util.Map;
27-
import java.util.Optional;
28-
import java.util.Set;
23+
import java.util.*;
2924

3025
import javax.servlet.http.HttpServletRequest;
3126

@@ -241,7 +236,9 @@ protected void getPaths(Map<String, Object> restControllers) {
241236
* @param map the map
242237
*/
243238
protected void calculatePath(Map<String, Object> restControllers, Map<RequestMappingInfo, HandlerMethod> map) {
244-
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
239+
List<Map.Entry<RequestMappingInfo, HandlerMethod>> entries = new ArrayList<>(map.entrySet());
240+
entries.sort(byReversedRequestMappingInfos());
241+
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : entries) {
245242
RequestMappingInfo requestMappingInfo = entry.getKey();
246243
HandlerMethod handlerMethod = entry.getValue();
247244
PatternsRequestCondition patternsRequestCondition = requestMappingInfo.getPatternsCondition();
@@ -265,6 +262,12 @@ && isFilterCondition(handlerMethod, operationPath, produces, consumes, headers))
265262
}
266263
}
267264

265+
private Comparator<Map.Entry<RequestMappingInfo, HandlerMethod>> byReversedRequestMappingInfos() {
266+
return Comparator.<Map.Entry<RequestMappingInfo, HandlerMethod>, String>
267+
comparing(a -> a.getKey().toString())
268+
.reversed();
269+
}
270+
268271
/**
269272
* Is rest controller boolean.
270273
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.api.app136;
20+
21+
import org.springframework.web.bind.annotation.GetMapping;
22+
import org.springframework.web.bind.annotation.RequestParam;
23+
import org.springframework.web.bind.annotation.RestController;
24+
25+
@RestController
26+
public class OperationIdController {
27+
28+
@GetMapping(path = "/test_0") // gets operationId opIdTest_3
29+
public String opIdTest() {
30+
return "";
31+
}
32+
33+
@GetMapping(path = "/test_1") // gets operationId opIdTest_2
34+
public String opIdTest(@RequestParam String param) {
35+
return "";
36+
}
37+
38+
@GetMapping(path = "/test_2") // gets operationId opIdTest_1
39+
public String opIdTest(@RequestParam Integer param) {
40+
return "";
41+
}
42+
43+
@GetMapping(path = "/test_3") // gets operationId opIdTest
44+
public String opIdTest(@RequestParam Boolean param) {
45+
return "";
46+
}
47+
48+
}

0 commit comments

Comments
 (0)