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

Commit

Permalink
JacksonModelAttributeSnippet now resolves all types (#439)
Browse files Browse the repository at this point in the history
Co-authored-by: Juraj Misur <juraj.misur@gmail.com>
  • Loading branch information
mustaphazorgati and jmisur authored Feb 17, 2021
1 parent 31e3a4e commit fc4c56f
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 48 deletions.
2 changes: 1 addition & 1 deletion spring-auto-restdocs-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<groupId>capital.scalable</groupId>
<artifactId>spring-auto-restdocs-parent</artifactId>
<version>2.0.10-SNAPSHOT</version>
<relativePath>..</relativePath>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>spring-auto-restdocs-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public EmbeddedSnippet failOnUndocumentedFields(boolean failOnUndocumentedFields
}

@Override
protected Type getType(HandlerMethod method) {
return documentationType;
protected Type[] getType(HandlerMethod method) {
return documentationType == null ? null : new Type[]{documentationType};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ public LinksSnippet failOnUndocumentedFields(boolean failOnUndocumentedFields) {
}

@Override
protected Type getType(HandlerMethod method) {
return documentationType;
protected Type[] getType(HandlerMethod method) {
return documentationType == null ? null : new Type[] {documentationType};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
import static org.slf4j.LoggerFactory.getLogger;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import capital.scalable.restdocs.constraints.ConstraintReader;
import capital.scalable.restdocs.i18n.SnippetTranslationResolver;
Expand Down Expand Up @@ -75,8 +78,16 @@ public FieldDocumentationGenerator(

public FieldDescriptors generateDocumentation(Type baseType, TypeFactory typeFactory)
throws JsonMappingException {
JavaType javaBaseType = typeFactory.constructType(baseType);
List<JavaType> types = resolveAllTypes(javaBaseType, typeFactory, typeMapping);
return generateDocumentation(new Type[]{baseType}, typeFactory);
}

public FieldDescriptors generateDocumentation(Type[] baseTypes, TypeFactory typeFactory)
throws JsonMappingException {
List<JavaType> types = Arrays.stream(baseTypes)
.map(typeFactory::constructType)
.map(type -> resolveAllTypes(type, typeFactory, typeMapping))
.flatMap(Collection::stream)
.collect(Collectors.toList());
FieldDescriptors result = new FieldDescriptors();

FieldDocumentationVisitorWrapper visitorWrapper = FieldDocumentationVisitorWrapper.create(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import static capital.scalable.restdocs.util.FieldDescriptorUtil.assertAllDocumented;

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Stream;
Expand Down Expand Up @@ -72,16 +73,16 @@ protected FieldDescriptors createFieldDescriptors(Operation operation,
TypeMapping typeMapping = getTypeMapping(operation);
JsonProperty.Access skipAcessor = getSkipAcessor();

Type type = getType(handlerMethod);
if (type == null) {
Type[] types = getType(handlerMethod);
if (types == null || types.length == 0) {
return new FieldDescriptors();
}

try {
FieldDocumentationGenerator generator = new FieldDocumentationGenerator(
objectMapper.writer(), objectMapper.getDeserializationConfig(), javadocReader,
constraintReader, typeMapping, translationResolver, skipAcessor);
FieldDescriptors fieldDescriptors = generator.generateDocumentation(type, objectMapper.getTypeFactory());
FieldDescriptors fieldDescriptors = generator.generateDocumentation(types, objectMapper.getTypeFactory());

if (shouldFailOnUndocumentedFields()) {
assertAllDocumented(fieldDescriptors.values(),
Expand All @@ -93,7 +94,7 @@ protected FieldDescriptors createFieldDescriptors(Operation operation,
}
}

protected abstract Type getType(HandlerMethod method);
protected abstract Type[] getType(HandlerMethod method);

protected abstract boolean shouldFailOnUndocumentedFields();

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

@Override
public boolean hasContent(Operation operation) {
return getType(getHandlerMethod(operation)) != null;
Type[] type = getType(getHandlerMethod(operation));
return type != null && type.length > 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import capital.scalable.restdocs.jackson.FieldDescriptors;
import capital.scalable.restdocs.util.HandlerMethodUtil;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.restdocs.operation.Operation;
import org.springframework.web.bind.annotation.ModelAttribute;
Expand All @@ -59,17 +60,16 @@ public JacksonModelAttributeSnippet(Collection<HandlerMethodArgumentResolver> ha
}

@Override
protected Type getType(HandlerMethod method) {
for (MethodParameter param : method.getMethodParameters()) {
if (isModelAttribute(param) || isProcessedAsModelAttribute(param)) {
return getType(param);
}
}
return null;
protected Type[] getType(HandlerMethod method) {
return Arrays.stream(method.getMethodParameters())
.filter(param -> isModelAttribute(param) || isProcessedAsModelAttribute(param))
.map(this::getType)
.toArray(Type[]::new);
}

private boolean isModelAttribute(MethodParameter param) {
return param.getParameterAnnotation(ModelAttribute.class) != null;
return param.getParameterAnnotation(ModelAttribute.class) != null
|| param.getParameterAnnotations().length == 0 && !BeanUtils.isSimpleProperty(param.getParameterType());
}

private boolean isProcessedAsModelAttribute(MethodParameter param) {
Expand All @@ -92,10 +92,7 @@ private Type getType(final MethodParameter param) {
protected boolean isRequestMethodGet(HandlerMethod method) {
RequestMapping requestMapping = method.getMethodAnnotation(RequestMapping.class);
return requestMapping == null
|| requestMapping.method() == null
|| Arrays.stream(requestMapping.method()).anyMatch(requestMethod -> {
return requestMethod == RequestMethod.GET;
});
|| Arrays.stream(requestMapping.method()).anyMatch(requestMethod -> requestMethod == RequestMethod.GET);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Type;
import java.util.Arrays;

import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.core.MethodParameter;
Expand Down Expand Up @@ -55,14 +56,13 @@ public JacksonRequestFieldSnippet failOnUndocumentedFields(boolean failOnUndocum
}

@Override
protected Type getType(HandlerMethod method) {
protected Type[] getType(HandlerMethod method) {
if (requestBodyType != null) {
return requestBodyType;
return new Type[]{requestBodyType};
}

for (MethodParameter param : method.getMethodParameters()) {
if (isRequestBody(param)) {
return getType(param);
return new Type[] {getType(param)};
}
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,37 +73,39 @@ public JacksonResponseFieldSnippet failOnUndocumentedFields(boolean failOnUndocu
}

@Override
protected Type getType(final HandlerMethod method) {
protected Type[] getType(final HandlerMethod method) {
if (responseBodyType != null) {
return responseBodyType;
return new Type[]{responseBodyType};
}

Class<?> returnType = method.getReturnType().getParameterType();
if (HttpEntity.class.isAssignableFrom(returnType)) {
return firstGenericType(method.getReturnType());
} else if (SPRING_PAGE_CLASSES.contains(returnType.getCanonicalName())) {
return firstGenericType(method.getReturnType());
} else if (SPRING_HATEOAS_CLASSES.contains(returnType.getCanonicalName())) {
return firstGenericType(method.getReturnType());
} else if (isCollection(returnType)) {
return (GenericArrayType) () -> firstGenericType(method.getReturnType());
} else if ("void".equals(returnType.getName())) {
return null;
} else if (REACTOR_MONO_CLASS.equals(returnType.getCanonicalName())) {
Class<?> methodReturnType = method.getReturnType().getParameterType();
Type returnType;
if (HttpEntity.class.isAssignableFrom(methodReturnType)) {
returnType = firstGenericType(method.getReturnType());
} else if (SPRING_PAGE_CLASSES.contains(methodReturnType.getCanonicalName())) {
returnType = firstGenericType(method.getReturnType());
} else if (SPRING_HATEOAS_CLASSES.contains(methodReturnType.getCanonicalName())) {
returnType = firstGenericType(method.getReturnType());
} else if (isCollection(methodReturnType)) {
returnType = (GenericArrayType) () -> firstGenericType(method.getReturnType());
} else if ("void".equals(methodReturnType.getName())) {
returnType = null;
} else if (REACTOR_MONO_CLASS.equals(methodReturnType.getCanonicalName())) {
Type type = firstGenericType(method.getReturnType());
if (type instanceof ParameterizedType) {
// can be Mono<ResponseEntity<FooBar>>
return ((ParameterizedType) type).getActualTypeArguments()[0];
returnType = ((ParameterizedType) type).getActualTypeArguments()[0];
} else {
return type;
returnType = type;
}
} else if (REACTOR_FLUX_CLASS.equals(returnType.getCanonicalName())) {
return (GenericArrayType) () -> firstGenericType(method.getReturnType());
} else if (REACTOR_FLUX_CLASS.equals(methodReturnType.getCanonicalName())) {
returnType = (GenericArrayType) () -> firstGenericType(method.getReturnType());
} else if (method.getReturnType().getGenericParameterType() instanceof TypeVariable) {
return firstGenericType(method.getReturnType());
returnType = firstGenericType(method.getReturnType());
} else {
return returnType;
returnType = methodReturnType;
}
return returnType == null ? null : new Type[]{returnType};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@
import org.springframework.restdocs.templates.TemplateFormat;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;

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

@Test
public void simpleRequestWithoutAnnotation() throws Exception {
HandlerMethod handlerMethod = createHandlerMethod("addItemWithoutAnnotation", Item.class);
mockFieldComment(Item.class, "field1", "A string");
mockFieldComment(Item.class, "field2", "An integer");
mockOptionalMessage(Item.class, "field1", "false");
mockConstraintMessage(Item.class, "field2", "A constraint");

new JacksonModelAttributeSnippet().document(operationBuilder
.attribute(HandlerMethod.class.getName(), handlerMethod)
.attribute(ObjectMapper.class.getName(), mapper)
.attribute(JavadocReader.class.getName(), javadocReader)
.attribute(ConstraintReader.class.getName(), constraintReader)
.build());

assertThat(this.generatedSnippets.snippet(AUTO_MODELATTRIBUTE)).is(
tableWithHeader("Parameter", "Type", "Optional", "Description")
.row("field1", "String", "false", "A string.")
.row("field2", "Integer", "true", "An integer.\n\nA constraint."));
}

@Test
public void simpleRequestWithoutAnnotationMixedWithOtherAnnotations() throws Exception {
HandlerMethod handlerMethod = createHandlerMethod("addItemWithoutAnnotationMixedWithOtherAnnotations", Item.class, ItemWithWeight.class);
mockFieldComment(Item.class, "field1", "A string");
mockFieldComment(Item.class, "field2", "An integer");
mockOptionalMessage(Item.class, "field1", "false");
mockConstraintMessage(Item.class, "field2", "A constraint");

new JacksonModelAttributeSnippet().document(operationBuilder
.attribute(HandlerMethod.class.getName(), handlerMethod)
.attribute(ObjectMapper.class.getName(), mapper)
.attribute(JavadocReader.class.getName(), javadocReader)
.attribute(ConstraintReader.class.getName(), constraintReader)
.build());

assertThat(this.generatedSnippets.snippet(AUTO_MODELATTRIBUTE)).is(
tableWithHeader("Parameter", "Type", "Optional", "Description")
.row("field1", "String", "false", "A string.")
.row("field2", "Integer", "true", "An integer.\n\nA constraint."));
}

@Test
public void simpleRequestWithEnum() throws Exception {
HandlerMethod handlerMethod = createHandlerMethod("addItemWithWeight",
Expand Down Expand Up @@ -260,6 +306,35 @@ public void accessors() throws Exception {
.row("bothWays", "String", "true", ""));
}

@Test
public void multipleModelAttributesWithout() throws Exception {
mockFieldComment(Item.class, "field1", "A string");
mockFieldComment(Item.class, "field2", "An integer");
mockOptionalMessage(Item.class, "field1", "false");

HandlerMethod handlerMethod = createHandlerMethod("withoutAnnotation", Item.class, ParentItem.class,
DeprecatedItem.class);

HandlerMethodArgumentResolver modelAttributeMethodProcessor = new ServletModelAttributeMethodProcessor(true);
new JacksonModelAttributeSnippet(singletonList(modelAttributeMethodProcessor), false).document(operationBuilder
.attribute(HandlerMethod.class.getName(), handlerMethod)
.attribute(ObjectMapper.class.getName(), mapper)
.attribute(JavadocReader.class.getName(), javadocReader)
.attribute(ConstraintReader.class.getName(), constraintReader)
.build());

assertThat(this.generatedSnippets.snippet(AUTO_MODELATTRIBUTE)).is(
tableWithHeader("Parameter", "Type", "Optional", "Description")
.row("field1", "String", "false", "A string.")
.row("field2", "Integer", "true", "An integer.")
.row("type", "String", "true", "")
.row("commonField", "String", "true", "")
.row("subItem1Field", "Boolean", "true", "")
.row("subItem2Field", "Integer", "true", "")
.row("index", "Integer", "true", "**Deprecated.**.")
);
}

private void mockConstraintMessage(Class<?> type, String fieldName, String comment) {
when(constraintReader.getConstraintMessages(type, fieldName))
.thenReturn(singletonList(comment));
Expand Down Expand Up @@ -291,6 +366,14 @@ public void addItem(@ModelAttribute Item item) {
// NOOP
}

public void addItemWithoutAnnotation(Item item) {
// NOOP
}

public void addItemWithoutAnnotationMixedWithOtherAnnotations(Item item, @RequestBody ItemWithWeight otherParameter) {
// NOOP
}

@PostMapping
public void addItemPost(@ModelAttribute Item item) {
// NOOP
Expand Down Expand Up @@ -319,6 +402,14 @@ public void removeItem(@ModelAttribute DeprecatedItem item) {
public void accessors(@ModelAttribute ReadWriteAccessors accessors) {
// NOOP
}

public void withoutAnnotation(
Item item,
ParentItem parentItem,
DeprecatedItem deprecatedItem
) {
// NOOP
}
}

private static class ProcessingCommand {
Expand Down

0 comments on commit fc4c56f

Please sign in to comment.