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

Commit fc4c56f

Browse files
JacksonModelAttributeSnippet now resolves all types (#439)
Co-authored-by: Juraj Misur <[email protected]>
1 parent 31e3a4e commit fc4c56f

File tree

9 files changed

+151
-48
lines changed

9 files changed

+151
-48
lines changed

spring-auto-restdocs-core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<groupId>capital.scalable</groupId>
99
<artifactId>spring-auto-restdocs-parent</artifactId>
1010
<version>2.0.10-SNAPSHOT</version>
11-
<relativePath>..</relativePath>
11+
<relativePath>../pom.xml</relativePath>
1212
</parent>
1313

1414
<artifactId>spring-auto-restdocs-core</artifactId>

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/hypermedia/EmbeddedSnippet.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ public EmbeddedSnippet failOnUndocumentedFields(boolean failOnUndocumentedFields
5151
}
5252

5353
@Override
54-
protected Type getType(HandlerMethod method) {
55-
return documentationType;
54+
protected Type[] getType(HandlerMethod method) {
55+
return documentationType == null ? null : new Type[]{documentationType};
5656
}
5757

5858
@Override

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/hypermedia/LinksSnippet.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ public LinksSnippet failOnUndocumentedFields(boolean failOnUndocumentedFields) {
5050
}
5151

5252
@Override
53-
protected Type getType(HandlerMethod method) {
54-
return documentationType;
53+
protected Type[] getType(HandlerMethod method) {
54+
return documentationType == null ? null : new Type[] {documentationType};
5555
}
5656

5757
@Override

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationGenerator.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@
2323
import static org.slf4j.LoggerFactory.getLogger;
2424

2525
import java.lang.reflect.Type;
26+
import java.util.ArrayList;
2627
import java.util.Arrays;
28+
import java.util.Collection;
2729
import java.util.HashSet;
2830
import java.util.List;
2931
import java.util.Set;
32+
import java.util.stream.Collectors;
3033

3134
import capital.scalable.restdocs.constraints.ConstraintReader;
3235
import capital.scalable.restdocs.i18n.SnippetTranslationResolver;
@@ -75,8 +78,16 @@ public FieldDocumentationGenerator(
7578

7679
public FieldDescriptors generateDocumentation(Type baseType, TypeFactory typeFactory)
7780
throws JsonMappingException {
78-
JavaType javaBaseType = typeFactory.constructType(baseType);
79-
List<JavaType> types = resolveAllTypes(javaBaseType, typeFactory, typeMapping);
81+
return generateDocumentation(new Type[]{baseType}, typeFactory);
82+
}
83+
84+
public FieldDescriptors generateDocumentation(Type[] baseTypes, TypeFactory typeFactory)
85+
throws JsonMappingException {
86+
List<JavaType> types = Arrays.stream(baseTypes)
87+
.map(typeFactory::constructType)
88+
.map(type -> resolveAllTypes(type, typeFactory, typeMapping))
89+
.flatMap(Collection::stream)
90+
.collect(Collectors.toList());
8091
FieldDescriptors result = new FieldDescriptors();
8192

8293
FieldDocumentationVisitorWrapper visitorWrapper = FieldDocumentationVisitorWrapper.create(

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import static capital.scalable.restdocs.util.FieldDescriptorUtil.assertAllDocumented;
2929

3030
import java.lang.reflect.Type;
31+
import java.util.Arrays;
3132
import java.util.Collection;
3233
import java.util.Map;
3334
import java.util.stream.Stream;
@@ -72,16 +73,16 @@ protected FieldDescriptors createFieldDescriptors(Operation operation,
7273
TypeMapping typeMapping = getTypeMapping(operation);
7374
JsonProperty.Access skipAcessor = getSkipAcessor();
7475

75-
Type type = getType(handlerMethod);
76-
if (type == null) {
76+
Type[] types = getType(handlerMethod);
77+
if (types == null || types.length == 0) {
7778
return new FieldDescriptors();
7879
}
7980

8081
try {
8182
FieldDocumentationGenerator generator = new FieldDocumentationGenerator(
8283
objectMapper.writer(), objectMapper.getDeserializationConfig(), javadocReader,
8384
constraintReader, typeMapping, translationResolver, skipAcessor);
84-
FieldDescriptors fieldDescriptors = generator.generateDocumentation(type, objectMapper.getTypeFactory());
85+
FieldDescriptors fieldDescriptors = generator.generateDocumentation(types, objectMapper.getTypeFactory());
8586

8687
if (shouldFailOnUndocumentedFields()) {
8788
assertAllDocumented(fieldDescriptors.values(),
@@ -93,7 +94,7 @@ protected FieldDescriptors createFieldDescriptors(Operation operation,
9394
}
9495
}
9596

96-
protected abstract Type getType(HandlerMethod method);
97+
protected abstract Type[] getType(HandlerMethod method);
9798

9899
protected abstract boolean shouldFailOnUndocumentedFields();
99100

@@ -113,6 +114,7 @@ public String getFileName() {
113114

114115
@Override
115116
public boolean hasContent(Operation operation) {
116-
return getType(getHandlerMethod(operation)) != null;
117+
Type[] type = getType(getHandlerMethod(operation));
118+
return type != null && type.length > 0;
117119
}
118120
}

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

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
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;
3738
import org.springframework.core.MethodParameter;
3839
import org.springframework.restdocs.operation.Operation;
3940
import org.springframework.web.bind.annotation.ModelAttribute;
@@ -59,17 +60,16 @@ public JacksonModelAttributeSnippet(Collection<HandlerMethodArgumentResolver> ha
5960
}
6061

6162
@Override
62-
protected Type getType(HandlerMethod method) {
63-
for (MethodParameter param : method.getMethodParameters()) {
64-
if (isModelAttribute(param) || isProcessedAsModelAttribute(param)) {
65-
return getType(param);
66-
}
67-
}
68-
return null;
63+
protected Type[] getType(HandlerMethod method) {
64+
return Arrays.stream(method.getMethodParameters())
65+
.filter(param -> isModelAttribute(param) || isProcessedAsModelAttribute(param))
66+
.map(this::getType)
67+
.toArray(Type[]::new);
6968
}
7069

7170
private boolean isModelAttribute(MethodParameter param) {
72-
return param.getParameterAnnotation(ModelAttribute.class) != null;
71+
return param.getParameterAnnotation(ModelAttribute.class) != null
72+
|| param.getParameterAnnotations().length == 0 && !BeanUtils.isSimpleProperty(param.getParameterType());
7373
}
7474

7575
private boolean isProcessedAsModelAttribute(MethodParameter param) {
@@ -92,10 +92,7 @@ private Type getType(final MethodParameter param) {
9292
protected boolean isRequestMethodGet(HandlerMethod method) {
9393
RequestMapping requestMapping = method.getMethodAnnotation(RequestMapping.class);
9494
return requestMapping == null
95-
|| requestMapping.method() == null
96-
|| Arrays.stream(requestMapping.method()).anyMatch(requestMethod -> {
97-
return requestMethod == RequestMethod.GET;
98-
});
95+
|| Arrays.stream(requestMapping.method()).anyMatch(requestMethod -> requestMethod == RequestMethod.GET);
9996
}
10097

10198
@Override

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import java.lang.reflect.GenericArrayType;
2626
import java.lang.reflect.Type;
27+
import java.util.Arrays;
2728

2829
import com.fasterxml.jackson.annotation.JsonProperty;
2930
import org.springframework.core.MethodParameter;
@@ -55,14 +56,13 @@ public JacksonRequestFieldSnippet failOnUndocumentedFields(boolean failOnUndocum
5556
}
5657

5758
@Override
58-
protected Type getType(HandlerMethod method) {
59+
protected Type[] getType(HandlerMethod method) {
5960
if (requestBodyType != null) {
60-
return requestBodyType;
61+
return new Type[]{requestBodyType};
6162
}
62-
6363
for (MethodParameter param : method.getMethodParameters()) {
6464
if (isRequestBody(param)) {
65-
return getType(param);
65+
return new Type[] {getType(param)};
6666
}
6767
}
6868
return null;

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

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -73,37 +73,39 @@ public JacksonResponseFieldSnippet failOnUndocumentedFields(boolean failOnUndocu
7373
}
7474

7575
@Override
76-
protected Type getType(final HandlerMethod method) {
76+
protected Type[] getType(final HandlerMethod method) {
7777
if (responseBodyType != null) {
78-
return responseBodyType;
78+
return new Type[]{responseBodyType};
7979
}
8080

81-
Class<?> returnType = method.getReturnType().getParameterType();
82-
if (HttpEntity.class.isAssignableFrom(returnType)) {
83-
return firstGenericType(method.getReturnType());
84-
} else if (SPRING_PAGE_CLASSES.contains(returnType.getCanonicalName())) {
85-
return firstGenericType(method.getReturnType());
86-
} else if (SPRING_HATEOAS_CLASSES.contains(returnType.getCanonicalName())) {
87-
return firstGenericType(method.getReturnType());
88-
} else if (isCollection(returnType)) {
89-
return (GenericArrayType) () -> firstGenericType(method.getReturnType());
90-
} else if ("void".equals(returnType.getName())) {
91-
return null;
92-
} else if (REACTOR_MONO_CLASS.equals(returnType.getCanonicalName())) {
81+
Class<?> methodReturnType = method.getReturnType().getParameterType();
82+
Type returnType;
83+
if (HttpEntity.class.isAssignableFrom(methodReturnType)) {
84+
returnType = firstGenericType(method.getReturnType());
85+
} else if (SPRING_PAGE_CLASSES.contains(methodReturnType.getCanonicalName())) {
86+
returnType = firstGenericType(method.getReturnType());
87+
} else if (SPRING_HATEOAS_CLASSES.contains(methodReturnType.getCanonicalName())) {
88+
returnType = firstGenericType(method.getReturnType());
89+
} else if (isCollection(methodReturnType)) {
90+
returnType = (GenericArrayType) () -> firstGenericType(method.getReturnType());
91+
} else if ("void".equals(methodReturnType.getName())) {
92+
returnType = null;
93+
} else if (REACTOR_MONO_CLASS.equals(methodReturnType.getCanonicalName())) {
9394
Type type = firstGenericType(method.getReturnType());
9495
if (type instanceof ParameterizedType) {
9596
// can be Mono<ResponseEntity<FooBar>>
96-
return ((ParameterizedType) type).getActualTypeArguments()[0];
97+
returnType = ((ParameterizedType) type).getActualTypeArguments()[0];
9798
} else {
98-
return type;
99+
returnType = type;
99100
}
100-
} else if (REACTOR_FLUX_CLASS.equals(returnType.getCanonicalName())) {
101-
return (GenericArrayType) () -> firstGenericType(method.getReturnType());
101+
} else if (REACTOR_FLUX_CLASS.equals(methodReturnType.getCanonicalName())) {
102+
returnType = (GenericArrayType) () -> firstGenericType(method.getReturnType());
102103
} else if (method.getReturnType().getGenericParameterType() instanceof TypeVariable) {
103-
return firstGenericType(method.getReturnType());
104+
returnType = firstGenericType(method.getReturnType());
104105
} else {
105-
return returnType;
106+
returnType = methodReturnType;
106107
}
108+
return returnType == null ? null : new Type[]{returnType};
107109
}
108110

109111
@Override

spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/payload/JacksonModelAttributeSnippetTest.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@
5050
import org.springframework.restdocs.templates.TemplateFormat;
5151
import org.springframework.web.bind.annotation.ModelAttribute;
5252
import org.springframework.web.bind.annotation.PostMapping;
53+
import org.springframework.web.bind.annotation.RequestBody;
54+
import org.springframework.web.bind.annotation.RequestParam;
5355
import org.springframework.web.method.HandlerMethod;
56+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
57+
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;
5458

5559
public class JacksonModelAttributeSnippetTest extends AbstractSnippetTests {
5660
private ObjectMapper mapper;
@@ -94,6 +98,48 @@ public void simpleRequest() throws Exception {
9498
.row("field2", "Integer", "true", "An integer.\n\nA constraint."));
9599
}
96100

101+
@Test
102+
public void simpleRequestWithoutAnnotation() throws Exception {
103+
HandlerMethod handlerMethod = createHandlerMethod("addItemWithoutAnnotation", Item.class);
104+
mockFieldComment(Item.class, "field1", "A string");
105+
mockFieldComment(Item.class, "field2", "An integer");
106+
mockOptionalMessage(Item.class, "field1", "false");
107+
mockConstraintMessage(Item.class, "field2", "A constraint");
108+
109+
new JacksonModelAttributeSnippet().document(operationBuilder
110+
.attribute(HandlerMethod.class.getName(), handlerMethod)
111+
.attribute(ObjectMapper.class.getName(), mapper)
112+
.attribute(JavadocReader.class.getName(), javadocReader)
113+
.attribute(ConstraintReader.class.getName(), constraintReader)
114+
.build());
115+
116+
assertThat(this.generatedSnippets.snippet(AUTO_MODELATTRIBUTE)).is(
117+
tableWithHeader("Parameter", "Type", "Optional", "Description")
118+
.row("field1", "String", "false", "A string.")
119+
.row("field2", "Integer", "true", "An integer.\n\nA constraint."));
120+
}
121+
122+
@Test
123+
public void simpleRequestWithoutAnnotationMixedWithOtherAnnotations() throws Exception {
124+
HandlerMethod handlerMethod = createHandlerMethod("addItemWithoutAnnotationMixedWithOtherAnnotations", Item.class, ItemWithWeight.class);
125+
mockFieldComment(Item.class, "field1", "A string");
126+
mockFieldComment(Item.class, "field2", "An integer");
127+
mockOptionalMessage(Item.class, "field1", "false");
128+
mockConstraintMessage(Item.class, "field2", "A constraint");
129+
130+
new JacksonModelAttributeSnippet().document(operationBuilder
131+
.attribute(HandlerMethod.class.getName(), handlerMethod)
132+
.attribute(ObjectMapper.class.getName(), mapper)
133+
.attribute(JavadocReader.class.getName(), javadocReader)
134+
.attribute(ConstraintReader.class.getName(), constraintReader)
135+
.build());
136+
137+
assertThat(this.generatedSnippets.snippet(AUTO_MODELATTRIBUTE)).is(
138+
tableWithHeader("Parameter", "Type", "Optional", "Description")
139+
.row("field1", "String", "false", "A string.")
140+
.row("field2", "Integer", "true", "An integer.\n\nA constraint."));
141+
}
142+
97143
@Test
98144
public void simpleRequestWithEnum() throws Exception {
99145
HandlerMethod handlerMethod = createHandlerMethod("addItemWithWeight",
@@ -260,6 +306,35 @@ public void accessors() throws Exception {
260306
.row("bothWays", "String", "true", ""));
261307
}
262308

309+
@Test
310+
public void multipleModelAttributesWithout() throws Exception {
311+
mockFieldComment(Item.class, "field1", "A string");
312+
mockFieldComment(Item.class, "field2", "An integer");
313+
mockOptionalMessage(Item.class, "field1", "false");
314+
315+
HandlerMethod handlerMethod = createHandlerMethod("withoutAnnotation", Item.class, ParentItem.class,
316+
DeprecatedItem.class);
317+
318+
HandlerMethodArgumentResolver modelAttributeMethodProcessor = new ServletModelAttributeMethodProcessor(true);
319+
new JacksonModelAttributeSnippet(singletonList(modelAttributeMethodProcessor), false).document(operationBuilder
320+
.attribute(HandlerMethod.class.getName(), handlerMethod)
321+
.attribute(ObjectMapper.class.getName(), mapper)
322+
.attribute(JavadocReader.class.getName(), javadocReader)
323+
.attribute(ConstraintReader.class.getName(), constraintReader)
324+
.build());
325+
326+
assertThat(this.generatedSnippets.snippet(AUTO_MODELATTRIBUTE)).is(
327+
tableWithHeader("Parameter", "Type", "Optional", "Description")
328+
.row("field1", "String", "false", "A string.")
329+
.row("field2", "Integer", "true", "An integer.")
330+
.row("type", "String", "true", "")
331+
.row("commonField", "String", "true", "")
332+
.row("subItem1Field", "Boolean", "true", "")
333+
.row("subItem2Field", "Integer", "true", "")
334+
.row("index", "Integer", "true", "**Deprecated.**.")
335+
);
336+
}
337+
263338
private void mockConstraintMessage(Class<?> type, String fieldName, String comment) {
264339
when(constraintReader.getConstraintMessages(type, fieldName))
265340
.thenReturn(singletonList(comment));
@@ -291,6 +366,14 @@ public void addItem(@ModelAttribute Item item) {
291366
// NOOP
292367
}
293368

369+
public void addItemWithoutAnnotation(Item item) {
370+
// NOOP
371+
}
372+
373+
public void addItemWithoutAnnotationMixedWithOtherAnnotations(Item item, @RequestBody ItemWithWeight otherParameter) {
374+
// NOOP
375+
}
376+
294377
@PostMapping
295378
public void addItemPost(@ModelAttribute Item item) {
296379
// NOOP
@@ -319,6 +402,14 @@ public void removeItem(@ModelAttribute DeprecatedItem item) {
319402
public void accessors(@ModelAttribute ReadWriteAccessors accessors) {
320403
// NOOP
321404
}
405+
406+
public void withoutAnnotation(
407+
Item item,
408+
ParentItem parentItem,
409+
DeprecatedItem deprecatedItem
410+
) {
411+
// NOOP
412+
}
322413
}
323414

324415
private static class ProcessingCommand {

0 commit comments

Comments
 (0)