Skip to content

Commit 990cd1e

Browse files
committed
Customize operation and parameters by the return value. Fixes #1372
1 parent b57f488 commit 990cd1e

File tree

16 files changed

+560
-7
lines changed

16 files changed

+560
-7
lines changed

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ protected void calculatePath(HandlerMethod handlerMethod, RouterOperation router
482482
buildCallbacks(openAPI, methodAttributes, operation, apiCallbacks);
483483

484484
// allow for customisation
485-
customiseOperation(operation, handlerMethod);
485+
operation = customiseOperation(operation, handlerMethod);
486486

487487
PathItem pathItemObject = buildPathItem(requestMethod, operation, operationPath, paths);
488488
paths.addPathItem(operationPath, pathItemObject);
@@ -836,7 +836,11 @@ protected Set<RequestMethod> getDefaultAllowedHttpMethods() {
836836
* @return the operation
837837
*/
838838
protected Operation customiseOperation(Operation operation, HandlerMethod handlerMethod) {
839-
operationCustomizers.ifPresent(customizers -> customizers.forEach(customizer -> customizer.customize(operation, handlerMethod)));
839+
if(operationCustomizers.isPresent()){
840+
List<OperationCustomizer> operationCustomizerList = operationCustomizers.get();
841+
for (OperationCustomizer operationCustomizer : operationCustomizerList)
842+
operation = operationCustomizer.customize(operation, handlerMethod);
843+
}
840844
return operation;
841845
}
842846

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ else if (!RequestMethod.GET.equals(requestMethod)) {
293293
applyBeanValidatorAnnotations(requestBodyInfo.getRequestBody(), parameterAnnotations, methodParameter.isOptional());
294294
}
295295

296-
customiseParameter(parameter, parameterInfo);
296+
customiseParameter(parameter, parameterInfo, operationParameters);
297297
}
298298
}
299299

@@ -365,11 +365,17 @@ public static Collection<Parameter> getHeaders(MethodAttributes methodAttributes
365365
*
366366
* @param parameter the parameter
367367
* @param parameterInfo the parameter info
368-
* @return the parameter
368+
* @param operationParameters the operation parameters
369369
*/
370-
protected Parameter customiseParameter(Parameter parameter, ParameterInfo parameterInfo) {
371-
parameterCustomizers.ifPresent(customizers -> customizers.forEach(customizer -> customizer.customize(parameter, parameterInfo.getMethodParameter())));
372-
return parameter;
370+
protected void customiseParameter(Parameter parameter, ParameterInfo parameterInfo, List<Parameter> operationParameters) {
371+
if (parameterCustomizers.isPresent()) {
372+
List<ParameterCustomizer> parameterCustomizerList = parameterCustomizers.get();
373+
int index = operationParameters.indexOf(parameter);
374+
for (ParameterCustomizer parameterCustomizer : parameterCustomizerList)
375+
parameter = parameterCustomizer.customize(parameter, parameterInfo.getMethodParameter());
376+
if (index != -1)
377+
operationParameters.set(index, parameter);
378+
}
373379
}
374380

375381
/**
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package test.org.springdoc.api.app178;
2+
3+
import java.lang.annotation.Retention;
4+
import java.lang.annotation.RetentionPolicy;
5+
import java.lang.reflect.Method;
6+
import java.util.function.Predicate;
7+
8+
import org.springdoc.core.GroupedOpenApi;
9+
import org.springdoc.core.customizers.OperationCustomizer;
10+
11+
import org.springframework.context.annotation.Bean;
12+
import org.springframework.web.bind.annotation.GetMapping;
13+
import org.springframework.web.bind.annotation.PostMapping;
14+
import org.springframework.web.bind.annotation.PutMapping;
15+
import org.springframework.web.bind.annotation.RestController;
16+
17+
@RestController
18+
public class AnnotatedController {
19+
20+
@Group1
21+
@GetMapping("/annotated")
22+
public String annotatedGet() {
23+
return "annotated";
24+
}
25+
26+
@Group1
27+
@PostMapping("/annotated")
28+
public String annotatedPost() {
29+
return "annotated";
30+
}
31+
32+
@Group2
33+
@PutMapping("/annotated")
34+
public String annotatedPut() {
35+
return "annotated";
36+
}
37+
38+
39+
@Bean
40+
public GroupedOpenApi group1OpenApi() {
41+
Predicate<Method> hidingCondition = method -> method.isAnnotationPresent(Group1.class);
42+
return GroupedOpenApi.builder()
43+
.group("annotatedGroup1")
44+
.addOperationCustomizer(getOperationCustomizer(hidingCondition))
45+
.build();
46+
}
47+
48+
@Bean
49+
public GroupedOpenApi group2OpenApi() {
50+
Predicate<Method> filterCondition = method -> method.isAnnotationPresent(Group2.class);
51+
return GroupedOpenApi.builder()
52+
.group("annotatedGroup2")
53+
.addOperationCustomizer(getOperationCustomizer(filterCondition))
54+
.build();
55+
}
56+
57+
@Bean
58+
public GroupedOpenApi group3OpenApi() {
59+
Predicate<Method> hidingCondition = method -> method.isAnnotationPresent(Group1.class) || method.isAnnotationPresent(Group2.class);
60+
return GroupedOpenApi.builder()
61+
.group("annotatedCombinedGroup")
62+
.addOperationCustomizer(getOperationCustomizer(hidingCondition))
63+
.build();
64+
}
65+
66+
private OperationCustomizer getOperationCustomizer(Predicate<Method> filterCondition) {
67+
return (operation, handlerMethod) -> filterCondition.test(handlerMethod.getMethod()) ? operation : null;
68+
}
69+
70+
@Retention(RetentionPolicy.RUNTIME)
71+
@interface Group1 {
72+
73+
}
74+
75+
@Retention(RetentionPolicy.RUNTIME)
76+
@interface Group2 {
77+
78+
}
79+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
24+
package test.org.springdoc.api.app178;
25+
26+
import org.junit.jupiter.api.Test;
27+
import org.springdoc.core.Constants;
28+
import test.org.springdoc.api.AbstractSpringDocTest;
29+
30+
import org.springframework.boot.autoconfigure.SpringBootApplication;
31+
32+
import static org.hamcrest.Matchers.is;
33+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
34+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
35+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
36+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
37+
38+
class SpringDocApp178Test extends AbstractSpringDocTest {
39+
40+
@SpringBootApplication
41+
static class SpringDocTestApp {}
42+
43+
@Test
44+
void testFilterOnlyPicksUpMatchedMethods() throws Exception {
45+
mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL + "/annotatedGroup1"))
46+
.andExpect(status().isOk())
47+
.andExpect(jsonPath("$.openapi", is("3.0.1")))
48+
.andExpect(content().json(getContent("results/app178-1.json"), true));
49+
}
50+
51+
@Test
52+
void testFilterOnlyPicksUpMatchedMethodsWithDifferentFilter() throws Exception {
53+
mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL + "/annotatedGroup2"))
54+
.andExpect(status().isOk())
55+
.andExpect(jsonPath("$.openapi", is("3.0.1")))
56+
.andExpect(content().json(getContent("results/app178-2.json"), true));
57+
}
58+
59+
@Test
60+
void testFilterOnlyPicksUpCombinedMatchedMethods() throws Exception {
61+
mockMvc.perform(get(Constants.DEFAULT_API_DOCS_URL + "/annotatedCombinedGroup"))
62+
.andExpect(status().isOk())
63+
.andExpect(jsonPath("$.openapi", is("3.0.1")))
64+
.andExpect(content().json(getContent("results/app178-3.json"), true));
65+
}
66+
67+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package test.org.springdoc.api.app179;
2+
3+
import org.springframework.web.bind.annotation.GetMapping;
4+
import org.springframework.web.bind.annotation.RestController;
5+
6+
7+
@RestController
8+
public class HelloController {
9+
@GetMapping("/test/{objId}")
10+
String test(@MyIdPathVariable MyObj obj) {
11+
return obj.getContent();
12+
}
13+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package test.org.springdoc.api.app179;
2+
3+
import java.util.List;
4+
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
7+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
8+
9+
@Configuration
10+
public class MyConfiguration implements WebMvcConfigurer {
11+
@Override
12+
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
13+
resolvers.add(new MyObjArgumentResolver());
14+
}
15+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package test.org.springdoc.api.app179;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Retention(RetentionPolicy.RUNTIME)
9+
@Target(ElementType.PARAMETER)
10+
public @interface MyIdPathVariable {
11+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package test.org.springdoc.api.app179;
2+
3+
public class MyObj {
4+
private final String id;
5+
private final String content;
6+
7+
public MyObj(String id, String content) {
8+
this.id = id;
9+
this.content = content;
10+
}
11+
12+
public String getId() {
13+
return id;
14+
}
15+
16+
public String getContent() {
17+
return content;
18+
}
19+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package test.org.springdoc.api.app179;
2+
3+
import org.springframework.core.MethodParameter;
4+
import org.springframework.web.bind.support.WebDataBinderFactory;
5+
import org.springframework.web.context.request.NativeWebRequest;
6+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
7+
import org.springframework.web.method.support.ModelAndViewContainer;
8+
9+
10+
public class MyObjArgumentResolver implements HandlerMethodArgumentResolver {
11+
12+
@Override
13+
public boolean supportsParameter(MethodParameter parameter) {
14+
return parameter.hasParameterAnnotation(MyIdPathVariable.class) &&
15+
MyObj.class.isAssignableFrom(parameter.getParameterType());
16+
}
17+
18+
@Override
19+
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
20+
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
21+
return new MyObj("id", "content");
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package test.org.springdoc.api.app179;
2+
3+
import io.swagger.v3.oas.models.media.StringSchema;
4+
import io.swagger.v3.oas.models.parameters.Parameter;
5+
import io.swagger.v3.oas.models.parameters.PathParameter;
6+
import org.springdoc.core.customizers.ParameterCustomizer;
7+
8+
import org.springframework.core.MethodParameter;
9+
import org.springframework.stereotype.Component;
10+
11+
@Component
12+
public class MyPathParameterCustomizer implements ParameterCustomizer {
13+
@Override
14+
public Parameter customize(Parameter parameterModel, MethodParameter methodParameter) {
15+
if (methodParameter.hasParameterAnnotation(MyIdPathVariable.class)) {
16+
Parameter alternativeParameter = new PathParameter();
17+
alternativeParameter.setName("objId");
18+
alternativeParameter.setSchema(new StringSchema());
19+
return alternativeParameter;
20+
}
21+
return parameterModel;
22+
}
23+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package test.org.springdoc.api.app179;
2+
3+
import test.org.springdoc.api.AbstractSpringDocTest;
4+
5+
import org.springframework.boot.autoconfigure.SpringBootApplication;
6+
7+
public class SpringDocApp179Test extends AbstractSpringDocTest {
8+
9+
@SpringBootApplication
10+
static class SpringDocTestApp {}
11+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
"/annotated": {
15+
"get": {
16+
"tags": [
17+
"annotated-controller"
18+
],
19+
"operationId": "annotatedGet",
20+
"responses": {
21+
"200": {
22+
"description": "OK",
23+
"content": {
24+
"*/*": {
25+
"schema": {
26+
"type": "string"
27+
}
28+
}
29+
}
30+
}
31+
}
32+
},
33+
"post": {
34+
"tags": [
35+
"annotated-controller"
36+
],
37+
"operationId": "annotatedPost",
38+
"responses": {
39+
"200": {
40+
"description": "OK",
41+
"content": {
42+
"*/*": {
43+
"schema": {
44+
"type": "string"
45+
}
46+
}
47+
}
48+
}
49+
}
50+
}
51+
}
52+
},
53+
"components": {}
54+
}

0 commit comments

Comments
 (0)