Skip to content

Commit f924b27

Browse files
author
bnasslahsen
committed
Improve support of Webflux with Functional Endpoints
1 parent 852bf83 commit f924b27

File tree

9 files changed

+551
-31
lines changed

9 files changed

+551
-31
lines changed

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

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,18 @@ protected void calculatePath(HandlerMethod handlerMethod, String operationPath,
247247
protected void calculatePath(String operationPath, Set<RequestMethod> requestMethods, io.swagger.v3.oas.annotations.Operation apiOperation, String[] methodConsumes, String[] methodProduces, String[] headers) {
248248
OpenAPI openAPI = openAPIBuilder.getCalculatedOpenAPI();
249249
Paths paths = openAPI.getPaths();
250+
Map<HttpMethod, Operation> operationMap = null;
251+
if (paths.containsKey(operationPath)) {
252+
PathItem pathItem = paths.get(operationPath);
253+
operationMap = pathItem.readOperationsMap();
254+
}
250255
for (RequestMethod requestMethod : requestMethods) {
256+
Operation existingOperation = getExistingOperation(operationMap, requestMethod);
251257
MethodAttributes methodAttributes = new MethodAttributes(springDocConfigProperties.getDefaultConsumesMediaType(), springDocConfigProperties.getDefaultProducesMediaType(), methodConsumes, methodProduces, headers);
252-
Operation operation = new Operation();
253-
openAPI = operationParser.parse(apiOperation, operation, openAPI, methodAttributes);
258+
methodAttributes.setMethodOverloaded(existingOperation != null);
259+
Operation operation = (existingOperation != null) ? existingOperation : new Operation();
260+
if (apiOperation != null)
261+
openAPI = operationParser.parse(apiOperation, operation, openAPI, methodAttributes);
254262
PathItem pathItemObject = buildPathItem(requestMethod, operation, operationPath, paths);
255263
paths.addPathItem(operationPath, pathItemObject);
256264
}
@@ -313,7 +321,7 @@ protected void getRouterFunctionPaths(String beanName, AbstractRouterFunctionVis
313321
calculatePath(routerOperationList.stream().map(routerOperation -> new org.springdoc.core.models.RouterOperation(routerOperation, routerFunctionVisitor.getRouterFunctionDatas().get(0))).collect(Collectors.toList()));
314322
else {
315323
List<org.springdoc.core.models.RouterOperation> operationList = routerOperationList.stream().map(org.springdoc.core.models.RouterOperation::new).collect(Collectors.toList());
316-
merge(routerFunctionVisitor.getRouterFunctionDatas(), operationList);
324+
mergeRouters(routerFunctionVisitor.getRouterFunctionDatas(), operationList);
317325
calculatePath(operationList);
318326
}
319327
}
@@ -490,29 +498,89 @@ protected Operation customiseOperation(Operation operation, HandlerMethod handle
490498
return operation;
491499
}
492500

493-
protected void merge(List<RouterFunctionData> routerFunctionDatas, List<org.springdoc.core.models.RouterOperation> routerOperationList) {
501+
protected void mergeRouters(List<RouterFunctionData> routerFunctionDatas, List<org.springdoc.core.models.RouterOperation> routerOperationList) {
494502
for (org.springdoc.core.models.RouterOperation routerOperation : routerOperationList) {
495-
List<RouterFunctionData> routerFunctionDataList = routerFunctionDatas.stream()
496-
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath()))
497-
.collect(Collectors.toList());
498-
if (!CollectionUtils.isEmpty(routerFunctionDataList)) {
499-
//Try with unique path in the route
503+
if (StringUtils.isNotBlank(routerOperation.getPath())) {
504+
// PATH
505+
List<RouterFunctionData> routerFunctionDataList = routerFunctionDatas.stream()
506+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath()))
507+
.collect(Collectors.toList());
500508
if (routerFunctionDataList.size() == 1)
501-
fillRouterOperation(routerFunctionDataList, routerOperation);
502-
//Try with unique path and RequestMethod
503-
else {
509+
fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
510+
else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getMethod())) {
511+
// PATH + METHOD
512+
routerFunctionDataList = routerFunctionDatas.stream()
513+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
514+
&& routerFunctionData1.getMethods()[0].equals(routerOperation.getMethod()[0]))
515+
.collect(Collectors.toList());
516+
if (routerFunctionDataList.size() == 1)
517+
fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
518+
else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getProduces())) {
519+
// PATH + METHOD + PRODUCES
520+
routerFunctionDataList = routerFunctionDatas.stream()
521+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
522+
&& routerFunctionData1.getMethods()[0].equals(routerOperation.getMethod()[0])
523+
&& routerFunctionData1.getProduces()[0].equals(routerOperation.getProduces()[0]))
524+
.collect(Collectors.toList());
525+
if (routerFunctionDataList.size() == 1)
526+
fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
527+
else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
528+
// PATH + METHOD + PRODUCES + CONSUMES
529+
routerFunctionDataList = routerFunctionDatas.stream()
530+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
531+
&& routerFunctionData1.getMethods()[0].equals(routerOperation.getMethod()[0])
532+
&& routerFunctionData1.getProduces()[0].equals(routerOperation.getProduces()[0])
533+
&& routerFunctionData1.getConsumes()[0].equals(routerOperation.getConsumes()[0]))
534+
.collect(Collectors.toList());
535+
if (routerFunctionDataList.size() == 1)
536+
fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
537+
}
538+
}
539+
else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
540+
// PATH + METHOD + CONSUMES
541+
routerFunctionDataList = routerFunctionDatas.stream()
542+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
543+
&& routerFunctionData1.getMethods()[0].equals(routerOperation.getMethod()[0])
544+
&& routerFunctionData1.getConsumes()[0].equals(routerOperation.getConsumes()[0]))
545+
.collect(Collectors.toList());
546+
if (routerFunctionDataList.size() == 1)
547+
fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
548+
}
549+
}
550+
else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getProduces())) {
551+
// PATH + PRODUCES
552+
routerFunctionDataList = routerFunctionDatas.stream()
553+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
554+
&& routerFunctionData1.getProduces()[0].equals(routerOperation.getProduces()[0]))
555+
.collect(Collectors.toList());
556+
if (routerFunctionDataList.size() == 1)
557+
fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
558+
else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
559+
// PATH + PRODUCES + CONSUMES
560+
routerFunctionDataList = routerFunctionDatas.stream()
561+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
562+
&& routerFunctionData1.getMethods()[0].equals(routerOperation.getMethod()[0])
563+
&& routerFunctionData1.getConsumes()[0].equals(routerOperation.getConsumes()[0])
564+
&& routerFunctionData1.getProduces()[0].equals(routerOperation.getProduces()[0]))
565+
.collect(Collectors.toList());
566+
if (routerFunctionDataList.size() == 1)
567+
fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
568+
}
569+
}
570+
else if (routerFunctionDataList.size() > 1 && ArrayUtils.isNotEmpty(routerOperation.getConsumes())) {
571+
// PATH + CONSUMES
504572
routerFunctionDataList = routerFunctionDatas.stream()
505-
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath()) && ArrayUtils.isNotEmpty(routerOperation.getMethod()) && routerFunctionData1.getMethods()[0].equals(routerOperation.getMethod()[0]))
573+
.filter(routerFunctionData1 -> routerFunctionData1.getPath().equals(routerOperation.getPath())
574+
&& routerFunctionData1.getConsumes()[0].equals(routerOperation.getConsumes()[0]))
506575
.collect(Collectors.toList());
507576
if (routerFunctionDataList.size() == 1)
508-
fillRouterOperation(routerFunctionDataList, routerOperation);
577+
fillRouterOperation(routerFunctionDataList.get(0), routerOperation);
509578
}
510579
}
511580
}
512581
}
513582

514-
private void fillRouterOperation(List<RouterFunctionData> routerFunctionDataList, org.springdoc.core.models.RouterOperation routerOperation) {
515-
RouterFunctionData routerFunctionData = routerFunctionDataList.get(0);
583+
private void fillRouterOperation(RouterFunctionData routerFunctionData, org.springdoc.core.models.RouterOperation routerOperation) {
516584
if (ArrayUtils.isEmpty(routerOperation.getConsumes()))
517585
routerOperation.setConsumes(routerFunctionData.getConsumes());
518586
if (ArrayUtils.isEmpty(routerOperation.getProduces()))

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,7 @@ private Map<String, ApiResponse> computeResponse(Components components, MethodPa
141141
continue;
142142
}
143143
apiResponse.setDescription(propertyResolverUtils.resolve(apiResponseAnnotations.description()));
144-
io.swagger.v3.oas.annotations.media.Content[] contentdoc = apiResponseAnnotations.content();
145-
buildContentFromDoc(components, apiResponsesOp, methodAttributes,
146-
apiResponseAnnotations, apiResponse, contentdoc);
144+
buildContentFromDoc(components, apiResponsesOp, methodAttributes, apiResponseAnnotations, apiResponse);
147145
Map<String, Object> extensions = AnnotationsUtils.getExtensions(apiResponseAnnotations.extensions());
148146
if (!CollectionUtils.isEmpty(extensions))
149147
apiResponse.extensions(extensions);
@@ -156,10 +154,12 @@ private Map<String, ApiResponse> computeResponse(Components components, MethodPa
156154
return apiResponsesOp;
157155
}
158156

159-
private void buildContentFromDoc(Components components, ApiResponses apiResponsesOp,
157+
public static void buildContentFromDoc(Components components, ApiResponses apiResponsesOp,
160158
MethodAttributes methodAttributes,
161-
io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotations, ApiResponse apiResponse,
162-
io.swagger.v3.oas.annotations.media.Content[] contentdoc) {
159+
io.swagger.v3.oas.annotations.responses.ApiResponse apiResponseAnnotations,
160+
ApiResponse apiResponse) {
161+
162+
io.swagger.v3.oas.annotations.media.Content[] contentdoc = apiResponseAnnotations.content();
163163
Optional<Content> optionalContent = SpringDocAnnotationsUtils.getContent(contentdoc, new String[0],
164164
methodAttributes.getMethodProduces(), null, components, methodAttributes.getJsonViewAnnotation());
165165
if (apiResponsesOp.containsKey(apiResponseAnnotations.responseCode())) {
@@ -301,7 +301,7 @@ else if (CollectionUtils.isEmpty(apiResponse.getContent()))
301301
apiResponsesOp.addApiResponse(httpCode, apiResponse);
302302
}
303303

304-
private void mergeSchema(Content existingContent, Schema<?> schemaN, String mediaTypeStr) {
304+
private static void mergeSchema(Content existingContent, Schema<?> schemaN, String mediaTypeStr) {
305305
if (existingContent.containsKey(mediaTypeStr)) {
306306
io.swagger.v3.oas.models.media.MediaType mediaType = existingContent.get(mediaTypeStr);
307307
if (!schemaN.equals(mediaType.getSchema())) {

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -279,10 +279,15 @@ private Set<String> extractOperationIdFromPathItem(PathItem path) {
279279
}
280280

281281
private Optional<ApiResponses> getApiResponses(
282-
final io.swagger.v3.oas.annotations.responses.ApiResponse[] responses, String[] classProduces,
283-
String[] methodProduces, Components components) {
282+
final io.swagger.v3.oas.annotations.responses.ApiResponse[] responses,
283+
MethodAttributes methodAttributes, Operation operation, Components components) {
284284

285285
ApiResponses apiResponsesObject = new ApiResponses();
286+
String[] classProduces = methodAttributes.getClassProduces();
287+
String[] methodProduces = methodAttributes.getMethodProduces();
288+
289+
ApiResponses apiResponsesOp = operation.getResponses();
290+
286291
for (io.swagger.v3.oas.annotations.responses.ApiResponse response : responses) {
287292
ApiResponse apiResponseObject = new ApiResponse();
288293
if (StringUtils.isNotBlank(response.ref())) {
@@ -292,10 +297,14 @@ private Optional<ApiResponses> getApiResponses(
292297
setDescription(response, apiResponseObject);
293298
setExtensions(response, apiResponseObject);
294299

295-
SpringDocAnnotationsUtils.getContent(response.content(),
296-
classProduces == null ? new String[0] : classProduces,
297-
methodProduces == null ? new String[0] : methodProduces, null, components, null)
298-
.ifPresent(apiResponseObject::content);
300+
if (apiResponsesOp == null)
301+
SpringDocAnnotationsUtils.getContent(response.content(),
302+
classProduces == null ? new String[0] : classProduces,
303+
methodProduces == null ? new String[0] : methodProduces, null, components, null)
304+
.ifPresent(apiResponseObject::content);
305+
else
306+
GenericResponseBuilder.buildContentFromDoc(components, apiResponsesOp, methodAttributes, response, apiResponseObject);
307+
299308
AnnotationsUtils.getHeaders(response.headers(), null).ifPresent(apiResponseObject::headers);
300309
// Make schema as string if empty
301310
calculateHeader(apiResponseObject);
@@ -368,10 +377,10 @@ private void setExtensions(io.swagger.v3.oas.annotations.responses.ApiResponse r
368377
}
369378
}
370379

380+
371381
private void buildResponse(Components components, io.swagger.v3.oas.annotations.Operation apiOperation,
372382
Operation operation, MethodAttributes methodAttributes) {
373-
getApiResponses(apiOperation.responses(), methodAttributes.getClassProduces(),
374-
methodAttributes.getMethodProduces(), components).ifPresent(responses -> {
383+
getApiResponses(apiOperation.responses(), methodAttributes, operation, components).ifPresent(responses -> {
375384
if (operation.getResponses() == null) {
376385
operation.setResponses(responses);
377386
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
*
3+
* *
4+
* * * Copyright 2019-2020 the original author or authors.
5+
* * *
6+
* * * Licensed under the Apache License, Version 2.0 (the "License");
7+
* * * you may not use this file except in compliance with the License.
8+
* * * You may obtain a copy of the License at
9+
* * *
10+
* * * https://www.apache.org/licenses/LICENSE-2.0
11+
* * *
12+
* * * Unless required by applicable law or agreed to in writing, software
13+
* * * distributed under the License is distributed on an "AS IS" BASIS,
14+
* * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* * * See the License for the specific language governing permissions and
16+
* * * limitations under the License.
17+
* *
18+
*
19+
*/
20+
21+
package test.org.springdoc.api.app73;
22+
23+
24+
import java.math.BigDecimal;
25+
import java.math.MathContext;
26+
import java.time.Instant;
27+
28+
public class Quote {
29+
30+
private static final MathContext MATH_CONTEXT = new MathContext(2);
31+
32+
private String ticker;
33+
34+
private BigDecimal price;
35+
36+
private Instant instant;
37+
38+
public Quote() {
39+
}
40+
41+
public Quote(String ticker, BigDecimal price) {
42+
this.ticker = ticker;
43+
this.price = price;
44+
}
45+
46+
public Quote(String ticker, Double price) {
47+
this(ticker, new BigDecimal(price, MATH_CONTEXT));
48+
}
49+
50+
public String getTicker() {
51+
return ticker;
52+
}
53+
54+
public void setTicker(String ticker) {
55+
this.ticker = ticker;
56+
}
57+
58+
public BigDecimal getPrice() {
59+
return price;
60+
}
61+
62+
public void setPrice(BigDecimal price) {
63+
this.price = price;
64+
}
65+
66+
public Instant getInstant() {
67+
return instant;
68+
}
69+
70+
public void setInstant(Instant instant) {
71+
this.instant = instant;
72+
}
73+
74+
@Override
75+
public String toString() {
76+
return "Quote{" +
77+
"ticker='" + ticker + '\'' +
78+
", price=" + price +
79+
", instant=" + instant +
80+
'}';
81+
}
82+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
*
3+
* *
4+
* * * Copyright 2019-2020 the original author or authors.
5+
* * *
6+
* * * Licensed under the Apache License, Version 2.0 (the "License");
7+
* * * you may not use this file except in compliance with the License.
8+
* * * You may obtain a copy of the License at
9+
* * *
10+
* * * https://www.apache.org/licenses/LICENSE-2.0
11+
* * *
12+
* * * Unless required by applicable law or agreed to in writing, software
13+
* * * distributed under the License is distributed on an "AS IS" BASIS,
14+
* * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* * * See the License for the specific language governing permissions and
16+
* * * limitations under the License.
17+
* *
18+
*
19+
*/
20+
21+
package test.org.springdoc.api.app73;
22+
23+
24+
import java.math.BigDecimal;
25+
import java.math.MathContext;
26+
import java.time.Duration;
27+
import java.time.Instant;
28+
import java.util.ArrayList;
29+
import java.util.List;
30+
import java.util.Random;
31+
import java.util.stream.Collectors;
32+
33+
import reactor.core.publisher.Flux;
34+
35+
import org.springframework.stereotype.Component;
36+
37+
@Component
38+
public class QuoteGenerator {
39+
40+
private final MathContext mathContext = new MathContext(2);
41+
42+
private final Random random = new Random();
43+
44+
private final List<Quote> prices = new ArrayList<>();
45+
46+
/**
47+
* Bootstraps the generator with tickers and initial prices
48+
*/
49+
public QuoteGenerator() {
50+
this.prices.add(new Quote("CTXS", 82.26));
51+
this.prices.add(new Quote("DELL", 63.74));
52+
this.prices.add(new Quote("GOOG", 847.24));
53+
this.prices.add(new Quote("MSFT", 65.11));
54+
this.prices.add(new Quote("ORCL", 45.71));
55+
this.prices.add(new Quote("RHT", 84.29));
56+
this.prices.add(new Quote("VMW", 92.21));
57+
}
58+
59+
60+
public Flux<Quote> fetchQuoteStream(Duration period) {
61+
62+
// We want to emit quotes with a specific period;
63+
// to do so, we create a Flux.interval
64+
return Flux.interval(period)
65+
// In case of back-pressure, drop events
66+
.onBackpressureDrop()
67+
// For each tick, generate a list of quotes
68+
.map(this::generateQuotes)
69+
// "flatten" that List<Quote> into a Flux<Quote>
70+
.flatMapIterable(quotes -> quotes)
71+
.log("io.spring.workshop.stockquotes");
72+
}
73+
74+
/*
75+
* Create quotes for all tickers at a single instant.
76+
*/
77+
private List<Quote> generateQuotes(long interval) {
78+
final Instant instant = Instant.now();
79+
return prices.stream()
80+
.map(baseQuote -> {
81+
BigDecimal priceChange = baseQuote.getPrice()
82+
.multiply(new BigDecimal(0.05 * this.random.nextDouble()), this.mathContext);
83+
Quote result = new Quote(baseQuote.getTicker(), baseQuote.getPrice().add(priceChange));
84+
result.setInstant(instant);
85+
return result;
86+
})
87+
.collect(Collectors.toList());
88+
}
89+
90+
}

0 commit comments

Comments
 (0)