From 01a0464f6d41a67293bbad2d8fd42bc1a91ad914 Mon Sep 17 00:00:00 2001 From: Juraj Misur Date: Sat, 26 Nov 2016 13:38:32 +0100 Subject: [PATCH] json subtype support --- .../payload/AbstractJacksonFieldSnippet.java | 67 ++++++++++++++----- .../snippet/StandardTableSnippet.java | 7 +- .../JacksonRequestFieldSnippetTest.java | 66 ++++++++++++++++++ 3 files changed, 122 insertions(+), 18 deletions(-) diff --git a/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/AbstractJacksonFieldSnippet.java b/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/AbstractJacksonFieldSnippet.java index 6ed80671..716bd2c6 100644 --- a/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/AbstractJacksonFieldSnippet.java +++ b/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/payload/AbstractJacksonFieldSnippet.java @@ -19,10 +19,13 @@ import static capital.scalable.restdocs.OperationAttributeHelper.getConstraintReader; import static capital.scalable.restdocs.OperationAttributeHelper.getJavadocReader; import static capital.scalable.restdocs.OperationAttributeHelper.getObjectMapper; +import static java.util.Collections.singletonList; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,8 +33,11 @@ import capital.scalable.restdocs.jackson.FieldDocumentationGenerator; import capital.scalable.restdocs.javadoc.JavadocReader; import capital.scalable.restdocs.snippet.StandardTableSnippet; +import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.type.TypeFactory; import org.springframework.core.MethodParameter; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.payload.FieldDescriptor; @@ -47,30 +53,30 @@ protected AbstractJacksonFieldSnippet(String type, Map attribute super(type + "-fields", attributes); } - protected List createFieldDescriptors(Operation operation, + protected Collection createFieldDescriptors(Operation operation, HandlerMethod handlerMethod) { - List fieldDescriptors = new ArrayList<>(); + ObjectMapper objectMapper = getObjectMapper(operation); + ObjectWriter writer = objectMapper.writer(); + TypeFactory typeFactory = objectMapper.getTypeFactory(); - Type type = getType(handlerMethod); - if (type != null) { - ObjectMapper objectMapper = getObjectMapper(operation); - JavadocReader javadocReader = getJavadocReader(operation); - ConstraintReader constraintReader = getConstraintReader(operation); + JavadocReader javadocReader = getJavadocReader(operation); + ConstraintReader constraintReader = getConstraintReader(operation); - try { - FieldDocumentationGenerator generator = new FieldDocumentationGenerator( - objectMapper.writer(), javadocReader, constraintReader); - - List descriptors = generator - .generateDocumentation(type, objectMapper.getTypeFactory()); + Map fieldDescriptors = new LinkedHashMap<>(); - fieldDescriptors.addAll(descriptors); + Type signatureType = getType(handlerMethod); + if (signatureType != null) { + try { + for (Type type : resolveActualTypes(signatureType)) { + resolveFieldDescriptors(fieldDescriptors, type, writer, typeFactory, + javadocReader, constraintReader); + } } catch (JsonMappingException e) { throw new JacksonFieldProcessingException("Error while parsing fields", e); } } - return fieldDescriptors; + return fieldDescriptors.values(); } protected Type firstGenericType(MethodParameter param) { @@ -78,4 +84,35 @@ protected Type firstGenericType(MethodParameter param) { } protected abstract Type getType(HandlerMethod method); + + private Collection resolveActualTypes(Type type) { + + if (type instanceof Class) { + JsonSubTypes jsonSubTypes = (JsonSubTypes) ((Class) type).getAnnotation( + JsonSubTypes.class); + if (jsonSubTypes != null) { + Collection types = new ArrayList<>(); + for (JsonSubTypes.Type subType : jsonSubTypes.value()) { + types.add(subType.value()); + } + return types; + } + } + + return singletonList(type); + } + + private void resolveFieldDescriptors(Map fieldDescriptors, + Type type, ObjectWriter writer, TypeFactory typeFactory, JavadocReader javadocReader, + ConstraintReader constraintReader) + throws JsonMappingException { + FieldDocumentationGenerator generator = new FieldDocumentationGenerator(writer, + javadocReader, constraintReader); + List descriptors = generator.generateDocumentation(type, typeFactory); + for (FieldDescriptor descriptor : descriptors) { + if (fieldDescriptors.get(descriptor.getPath()) == null) { + fieldDescriptors.put(descriptor.getPath(), descriptor); + } + } + } } diff --git a/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/snippet/StandardTableSnippet.java b/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/snippet/StandardTableSnippet.java index d320615c..ad021255 100644 --- a/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/snippet/StandardTableSnippet.java +++ b/spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/snippet/StandardTableSnippet.java @@ -23,6 +23,7 @@ import static org.apache.commons.lang3.StringUtils.join; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -44,7 +45,7 @@ protected StandardTableSnippet(String snippetName, Map attribute protected Map createModel(Operation operation) { HandlerMethod handlerMethod = getHandlerMethod(operation); - List fieldDescriptors = emptyList(); + Collection fieldDescriptors = emptyList(); if (handlerMethod != null) { fieldDescriptors = createFieldDescriptors(operation, handlerMethod); } @@ -52,7 +53,7 @@ protected Map createModel(Operation operation) { return createModel(handlerMethod, fieldDescriptors); } - protected abstract List createFieldDescriptors(Operation operation, + protected abstract Collection createFieldDescriptors(Operation operation, HandlerMethod handlerMethod); protected void enrichModel(Map model, HandlerMethod handlerMethod) { @@ -60,7 +61,7 @@ protected void enrichModel(Map model, HandlerMethod handlerMetho } private Map createModel(HandlerMethod handlerMethod, - List fieldDescriptors) { + Collection fieldDescriptors) { Map model = new HashMap<>(); enrichModel(model, handlerMethod); diff --git a/spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/payload/JacksonRequestFieldSnippetTest.java b/spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/payload/JacksonRequestFieldSnippetTest.java index ec051a30..5e0d1df8 100644 --- a/spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/payload/JacksonRequestFieldSnippetTest.java +++ b/spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/payload/JacksonRequestFieldSnippetTest.java @@ -16,6 +16,8 @@ package capital.scalable.restdocs.payload; +import static com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY; +import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME; import static java.util.Collections.singletonList; import static org.hamcrest.CoreMatchers.equalTo; import static org.mockito.Mockito.mock; @@ -27,6 +29,8 @@ import capital.scalable.restdocs.constraints.ConstraintReader; import capital.scalable.restdocs.javadoc.JavadocReader; import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.databind.ObjectMapper; import org.hibernate.validator.constraints.NotBlank; import org.junit.Test; @@ -122,6 +126,44 @@ public void listRequest() throws Exception { .build()); } + @Test + public void jsonSubTypesRequest() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker() + .withFieldVisibility(JsonAutoDetect.Visibility.ANY)); + + HandlerMethod handlerMethod = new HandlerMethod(new TestResource(), "addSubItem", + ParentItem.class); + JavadocReader javadocReader = mock(JavadocReader.class); + when(javadocReader.resolveFieldComment(ParentItem.class, "type")) + .thenReturn("A type"); + when(javadocReader.resolveFieldComment(ParentItem.class, "commonField")) + .thenReturn("A common field"); + when(javadocReader.resolveFieldComment(SubItem1.class, "subItem1Field")) + .thenReturn("A sub item 1 field"); + when(javadocReader.resolveFieldComment(SubItem2.class, "subItem2Field")) + .thenReturn("A sub item 2 field"); + + ConstraintReader constraintReader = mock(ConstraintReader.class); + + this.snippet.expectRequestFields().withContents( + tableWithHeader("Path", "Type", "Optional", "Description") + .row("type", "String", "true", "A type") + .row("commonField", "String", "true", "A common field") + .row("subItem1Field", "Boolean", "true", "A sub item 1 field") + .row("subItem2Field", "Integer", "true", "A sub item 2 field")); + + new JacksonRequestFieldSnippet().document(operationBuilder + .attribute(HandlerMethod.class.getName(), handlerMethod) + .attribute(ObjectMapper.class.getName(), mapper) + .attribute(JavadocReader.class.getName(), javadocReader) + .attribute(ConstraintReader.class.getName(), constraintReader) + .request("http://localhost") + .content("{\"type\":\"1\"}") + .build()); + } + + private static class TestResource { public void addItem(@RequestBody Item item) { @@ -135,6 +177,10 @@ public void addItems(@RequestBody List items) { public void addItem2() { // NOOP } + + public void addSubItem(@RequestBody ParentItem item) { + // NOOP + } } private static class Item { @@ -143,4 +189,24 @@ private static class Item { @Size(max = 10) private Integer field2; } + + + @JsonTypeInfo(use = NAME, include = PROPERTY, property = "type", visible = true) + @JsonSubTypes({ + @JsonSubTypes.Type(value = SubItem1.class, name = "1"), + @JsonSubTypes.Type(value = SubItem2.class, name = "2") + }) + private static abstract class ParentItem { + private String type; + private String commonField; + + } + + private static class SubItem1 extends ParentItem { + private Boolean subItem1Field; + } + + private static class SubItem2 extends ParentItem { + private Integer subItem2Field; + } }