Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit 753684c

Browse files
authored
Deduplicate sections if modelattribute is present (#393)
1 parent 0491e2b commit 753684c

File tree

12 files changed

+384
-94
lines changed

12 files changed

+384
-94
lines changed

samples/java-webmvc/src/main/asciidoc/items.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,5 @@ include::{snippets}/item-resource-test/validate-metadata/auto-section.adoc[]
7474
include::{snippets}/item-resource-test/clone-item/auto-section.adoc[]
7575
include::{snippets}/item-resource-test/get-hypermedia-item/auto-section.adoc[]
7676
include::{snippets}/item-resource-test/get-item-as-resource/auto-section.adoc[]
77+
include::{snippets}/item-resource-test/get-item-with-filter/auto-section.adoc[]
78+
include::{snippets}/item-resource-test/update-item-with-filter/auto-section.adoc[]

samples/java-webmvc/src/main/java/capital/scalable/restdocs/example/items/ItemResource.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.math.BigDecimal;
3333
import java.net.URI;
3434
import java.util.Collections;
35+
import java.util.List;
3536

3637
import capital.scalable.restdocs.example.common.Money;
3738
import capital.scalable.restdocs.example.constraints.English;
@@ -72,7 +73,7 @@ public class ItemResource {
7273
new Attributes("first item", 1, true, DECIMAL, Money.of(AMOUNT, "EUR"), ONE);
7374
private static final Metadata1 META = new Metadata1("1", "meta1");
7475
private static final ItemResponse ITEM =
75-
new ItemResponse("1", "main item", META, ATTRIBUTES, singletonList(CHILD), new String[]{"top-level"});
76+
new ItemResponse("1", "main item", META, ATTRIBUTES, singletonList(CHILD), new String[] { "top-level" });
7677

7778
/**
7879
* Returns item by ID.
@@ -104,7 +105,7 @@ public ItemResponse getItem(@PathVariable("id") @Id String id) {
104105
*/
105106
@GetMapping
106107
public ItemResponse[] allItems() {
107-
return new ItemResponse[]{ITEM, CHILD};
108+
return new ItemResponse[] { ITEM, CHILD };
108109
}
109110

110111
/**
@@ -283,7 +284,7 @@ public HypermediaItemResponse getHypermediaItem(@PathVariable("id") @Id String i
283284
response.add(linkTo(methodOn(ItemResource.class).getItem(id)).withRel("classicItem"));
284285
response.add(linkTo(methodOn(ItemResource.class).processSingleItem(id, null)).withRel("process"));
285286
if (embedded != null && embedded) {
286-
response.addEmbedded("children", new Object[]{CHILD});
287+
response.addEmbedded("children", new Object[] { CHILD });
287288
response.addEmbedded("attributes", ATTRIBUTES);
288289
response.addEmbedded("meta", META);
289290
}
@@ -303,6 +304,34 @@ public EntityModel<ItemResponse> getItemAsResource(@PathVariable("id") @Id Strin
303304
);
304305
}
305306

307+
/**
308+
* Returns item with filtering.
309+
* <p>
310+
* Shows how model attribute can be used with query parameters.
311+
*
312+
* @param name filter by name
313+
* @param filter additional filters
314+
* @return matching items
315+
*/
316+
@GetMapping("filtered")
317+
public List<ItemResponse> getItemWithFilter(@RequestParam String name, Filter filter) {
318+
return singletonList(ITEM);
319+
}
320+
321+
/**
322+
* Updates existing item only if filter applies.
323+
* <p>
324+
* Shows how model attribute can be used with request body.
325+
*
326+
* @param id Item ID.
327+
* @param itemUpdate Item information.
328+
* @param filter Additional filters.
329+
*/
330+
@PutMapping("filtered/{id}")
331+
public void updateItemWithFilter(@PathVariable("id") @Id String id,
332+
@RequestBody @Valid ItemUpdateRequest itemUpdate, Filter filter) {
333+
}
334+
306335
static class CloneData {
307336
/**
308337
* New item's name
@@ -353,4 +382,11 @@ public String getOutput() {
353382
@ResponseStatus(HttpStatus.NOT_FOUND)
354383
static class NotFoundException extends RuntimeException {
355384
}
385+
386+
static class Filter {
387+
/**
388+
* Only items with this tag should be returned.
389+
*/
390+
private String tag;
391+
}
356392
}

samples/java-webmvc/src/test/java/capital/scalable/restdocs/example/items/ItemResourceTest.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
99
* You may obtain a copy of the License at
10-
*
10+
*
1111
* http://www.apache.org/licenses/LICENSE-2.0
12-
*
12+
*
1313
* Unless required by applicable law or agreed to in writing, software
1414
* distributed under the License is distributed on an "AS IS" BASIS,
1515
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -242,4 +242,18 @@ public void getItemAsResource() throws Exception {
242242
links().documentationType(LinksDocumentation.class)
243243
));
244244
}
245+
246+
@Test
247+
public void getItemWithFilter() throws Exception {
248+
mockMvc.perform(get("/items/filtered?name=abc&tag=prod"))
249+
.andExpect(status().isOk());
250+
}
251+
252+
@Test
253+
public void updateItemWithFilter() throws Exception {
254+
mockMvc.perform(put("/items/filtered/1").with(userToken())
255+
.contentType(MediaType.APPLICATION_JSON)
256+
.content("{\"description\":\"Hot News\"}"))
257+
.andExpect(status().isOk());
258+
}
245259
}

samples/java-webmvc/src/test/java/capital/scalable/restdocs/example/testsupport/MockMvcBase.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@
5252

5353
import javax.servlet.Filter;
5454

55-
import java.util.Collection;
56-
5755
import capital.scalable.restdocs.example.items.Metadata;
5856
import capital.scalable.restdocs.example.items.Metadata3;
5957
import capital.scalable.restdocs.jackson.TypeMapping;
@@ -74,7 +72,7 @@
7472
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
7573
import org.springframework.util.Base64Utils;
7674
import org.springframework.web.context.WebApplicationContext;
77-
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
75+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
7876

7977
/**
8078
* Required set up code for MockMvc tests.
@@ -95,7 +93,7 @@ public abstract class MockMvcBase {
9593
private Filter springSecurityFilterChain;
9694

9795
@Autowired
98-
private Collection<HandlerMethodArgumentResolver> handlerMethodArgumentResolvers;
96+
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
9997

10098
protected MockMvc mockMvc;
10199

@@ -120,7 +118,7 @@ public void setUp() throws Exception {
120118
requestFields(), responseFields(), pathParameters(),
121119
requestParameters(), description(), methodAndPath(),
122120
section(), links(), embedded(), authorization(DEFAULT_AUTHORIZATION),
123-
modelAttribute(handlerMethodArgumentResolvers)))
121+
modelAttribute(requestMappingHandlerAdapter.getArgumentResolvers())))
124122
.build();
125123
}
126124

samples/kotlin-webmvc/src/test/java/capital/scalable/restdocs/example/testsupport/MockMvcBase.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder
6565
import org.springframework.test.web.servlet.setup.MockMvcBuilders
6666
import org.springframework.util.Base64Utils
6767
import org.springframework.web.context.WebApplicationContext
68-
import org.springframework.web.method.support.HandlerMethodArgumentResolver
68+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
6969
import javax.servlet.Filter
7070

7171
private const val DEFAULT_AUTHORIZATION = "Resource is public."
@@ -87,7 +87,7 @@ abstract class MockMvcBase {
8787
private lateinit var springSecurityFilterChain: Filter
8888

8989
@Autowired
90-
private lateinit var handlerMethodArgumentResolvers: Collection<HandlerMethodArgumentResolver>
90+
private lateinit var requestMappingHandlerAdapter: RequestMappingHandlerAdapter
9191

9292
protected lateinit var mockMvc: MockMvc
9393

@@ -112,7 +112,7 @@ abstract class MockMvcBase {
112112
requestFields(), responseFields(), pathParameters(),
113113
requestParameters(), description(), methodAndPath(),
114114
section(), authorization(DEFAULT_AUTHORIZATION),
115-
modelAttribute(handlerMethodArgumentResolvers)))
115+
modelAttribute(requestMappingHandlerAdapter.getArgumentResolvers())))
116116
.build()
117117
}
118118

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/JacksonModelAttributeSnippet.java

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
99
* You may obtain a copy of the License at
10-
*
10+
*
1111
* http://www.apache.org/licenses/LICENSE-2.0
12-
*
12+
*
1313
* Unless required by applicable law or agreed to in writing, software
1414
* distributed under the License is distributed on an "AS IS" BASIS,
1515
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -34,13 +34,13 @@
3434
import capital.scalable.restdocs.jackson.FieldDescriptors;
3535
import capital.scalable.restdocs.util.HandlerMethodUtil;
3636
import com.fasterxml.jackson.annotation.JsonProperty;
37-
import org.springframework.beans.BeanUtils;
3837
import org.springframework.core.MethodParameter;
3938
import org.springframework.restdocs.operation.Operation;
4039
import org.springframework.web.bind.annotation.ModelAttribute;
4140
import org.springframework.web.bind.annotation.RequestMapping;
4241
import org.springframework.web.bind.annotation.RequestMethod;
4342
import org.springframework.web.method.HandlerMethod;
43+
import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
4444
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
4545

4646
public class JacksonModelAttributeSnippet extends AbstractJacksonFieldSnippet {
@@ -51,7 +51,8 @@ public JacksonModelAttributeSnippet() {
5151
this(null, false);
5252
}
5353

54-
public JacksonModelAttributeSnippet(Collection<HandlerMethodArgumentResolver> handlerMethodArgumentResolvers, boolean failOnUndocumentedFields) {
54+
public JacksonModelAttributeSnippet(Collection<HandlerMethodArgumentResolver> handlerMethodArgumentResolvers,
55+
boolean failOnUndocumentedFields) {
5556
super(AUTO_MODELATTRIBUTE, null);
5657
this.failOnUndocumentedFields = failOnUndocumentedFields;
5758
this.handlerMethodArgumentResolvers = handlerMethodArgumentResolvers;
@@ -60,27 +61,26 @@ public JacksonModelAttributeSnippet(Collection<HandlerMethodArgumentResolver> ha
6061
@Override
6162
protected Type getType(HandlerMethod method) {
6263
for (MethodParameter param : method.getMethodParameters()) {
63-
if (isModelAttribute(param) || hasNoHandlerMethodArgumentResolver(param)) {
64+
if (isModelAttribute(param) || isProcessedAsModelAttribute(param)) {
6465
return getType(param);
6566
}
6667
}
6768
return null;
6869
}
6970

70-
7171
private boolean isModelAttribute(MethodParameter param) {
7272
return param.getParameterAnnotation(ModelAttribute.class) != null;
7373
}
7474

75-
private boolean hasNoHandlerMethodArgumentResolver(MethodParameter param) {
76-
return !BeanUtils.isSimpleProperty(param.getParameterType())
77-
&& this.handlerMethodArgumentResolvers != null
78-
&& this.handlerMethodArgumentResolvers
79-
.stream()
80-
.noneMatch(obj -> obj.supportsParameter(param));
75+
private boolean isProcessedAsModelAttribute(MethodParameter param) {
76+
return handlerMethodArgumentResolvers != null
77+
&& handlerMethodArgumentResolvers.stream()
78+
.filter(hmar -> hmar.supportsParameter(param))
79+
.findFirst()
80+
.filter(first -> first instanceof ModelAttributeMethodProcessor)
81+
.isPresent();
8182
}
8283

83-
8484
private Type getType(final MethodParameter param) {
8585
if (isCollection(param.getParameterType())) {
8686
return (GenericArrayType) () -> firstGenericType(param);
@@ -89,16 +89,11 @@ private Type getType(final MethodParameter param) {
8989
}
9090
}
9191

92-
/**
93-
* is request method get supported
94-
* @param method
95-
* @return
96-
*/
9792
protected boolean isRequestMethodGet(HandlerMethod method) {
9893
RequestMapping requestMapping = method.getMethodAnnotation(RequestMapping.class);
9994
return requestMapping == null
100-
|| requestMapping.method() == null
101-
|| Arrays.stream(requestMapping.method()).anyMatch(requestMethod -> {
95+
|| requestMapping.method() == null
96+
|| Arrays.stream(requestMapping.method()).anyMatch(requestMethod -> {
10297
return requestMethod == RequestMethod.GET;
10398
});
10499
}
@@ -127,16 +122,21 @@ public String getHeaderKey(Operation operation) {
127122
return "request-fields";
128123
}
129124

125+
@Override
126+
public boolean isMergeable() {
127+
return true; // mergeable with headers above
128+
}
129+
130130
@Override
131131
protected String[] getTranslationKeys() {
132-
return new String[]{
133-
"th-parameter",
134-
"th-type",
135-
"th-optional",
136-
"th-description",
137-
"pagination-request-adoc",
138-
"pagination-request-md",
139-
"no-params"
132+
return new String[] {
133+
"th-parameter",
134+
"th-type",
135+
"th-optional",
136+
"th-description",
137+
"pagination-request-adoc",
138+
"pagination-request-md",
139+
"no-params"
140140
};
141141
}
142142

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/JacksonRequestFieldSnippet.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
99
* You may obtain a copy of the License at
10-
*
10+
*
1111
* http://www.apache.org/licenses/LICENSE-2.0
12-
*
12+
*
1313
* Unless required by applicable law or agreed to in writing, software
1414
* distributed under the License is distributed on an "AS IS" BASIS,
1515
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -92,7 +92,7 @@ protected boolean shouldFailOnUndocumentedFields() {
9292

9393
@Override
9494
protected String[] getTranslationKeys() {
95-
return new String[]{
95+
return new String[] {
9696
"th-path",
9797
"th-type",
9898
"th-optional",
@@ -105,4 +105,9 @@ protected String[] getTranslationKeys() {
105105
protected JsonProperty.Access getSkipAcessor() {
106106
return JsonProperty.Access.READ_ONLY;
107107
}
108+
109+
@Override
110+
public boolean isMergeable() {
111+
return true;
112+
}
108113
}

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/request/RequestParametersSnippet.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
99
* You may obtain a copy of the License at
10-
*
10+
*
1111
* http://www.apache.org/licenses/LICENSE-2.0
12-
*
12+
*
1313
* Unless required by applicable law or agreed to in writing, software
1414
* distributed under the License is distributed on an "AS IS" BASIS,
1515
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -34,7 +34,6 @@
3434

3535
public class RequestParametersSnippet extends AbstractParameterSnippet<RequestParam> {
3636

37-
3837
private final boolean failOnUndocumentedParams;
3938

4039
public RequestParametersSnippet() {
@@ -106,7 +105,7 @@ protected String getDefaultValue(final RequestParam annotation) {
106105

107106
@Override
108107
protected String[] getTranslationKeys() {
109-
return new String[]{
108+
return new String[] {
110109
"th-parameter",
111110
"th-type",
112111
"th-optional",
@@ -116,4 +115,9 @@ protected String[] getTranslationKeys() {
116115
"no-params"
117116
};
118117
}
118+
119+
@Override
120+
public boolean isMergeable() {
121+
return true;
122+
}
119123
}

0 commit comments

Comments
 (0)