capital.scalable.restdocs.jackson.FieldDocumentationGeneratorTest.java Source code

Java tutorial

Introduction

Here is the source code for capital.scalable.restdocs.jackson.FieldDocumentationGeneratorTest.java

Source

/*
 * Copyright 2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package capital.scalable.restdocs.jackson;

import static capital.scalable.restdocs.constraints.ConstraintReader.CONSTRAINTS_ATTRIBUTE;
import static capital.scalable.restdocs.constraints.ConstraintReader.OPTIONAL_ATTRIBUTE;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;

import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import capital.scalable.restdocs.constraints.ConstraintReader;
import capital.scalable.restdocs.javadoc.JavadocReader;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.junit.Test;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.snippet.Attributes.Attribute;

public class FieldDocumentationGeneratorTest {

    @Test
    public void testGenerateDocumentationForPrimitiveTypes() throws Exception {
        // given
        ObjectMapper mapper = createMapper();
        JavadocReader javadocReader = mock(JavadocReader.class);
        when(javadocReader.resolveFieldComment(PrimitiveTypes.class, "stringField")).thenReturn("A string");
        when(javadocReader.resolveFieldComment(PrimitiveTypes.class, "booleanField")).thenReturn("A boolean");
        when(javadocReader.resolveFieldComment(PrimitiveTypes.class, "numberField1")).thenReturn("An integer");
        when(javadocReader.resolveFieldComment(PrimitiveTypes.class, "numberField2")).thenReturn("A decimal");

        ConstraintReader constraintReader = mock(ConstraintReader.class);

        FieldDocumentationGenerator generator = new FieldDocumentationGenerator(mapper.writer(), javadocReader,
                constraintReader);
        Type type = PrimitiveTypes.class;

        // when
        List<ExtendedFieldDescriptor> fieldDescriptions = cast(
                generator.generateDocumentation(type, mapper.getTypeFactory()));
        // then
        assertThat(fieldDescriptions.size(), is(4));
        assertThat(fieldDescriptions.get(0), is(descriptor("stringField", "String", "A string", "true")));
        assertThat(fieldDescriptions.get(1), is(descriptor("booleanField", "Boolean", "A boolean", "true")));
        assertThat(fieldDescriptions.get(2), is(descriptor("numberField1", "Integer", "An integer", "true")));
        assertThat(fieldDescriptions.get(3), is(descriptor("numberField2", "Decimal", "A decimal", "true")));
    }

    @Test
    public void testGenerateDocumentationForComposedTypes() throws Exception {
        // given
        ObjectMapper mapper = createMapper();
        JavadocReader javadocReader = mock(JavadocReader.class);
        when(javadocReader.resolveFieldComment(ComposedTypes.class, "objectField")).thenReturn("An object");
        when(javadocReader.resolveFieldComment(PrimitiveTypes.class, "stringField")).thenReturn("A string");
        when(javadocReader.resolveFieldComment(PrimitiveTypes.class, "booleanField")).thenReturn("A boolean");
        when(javadocReader.resolveFieldComment(PrimitiveTypes.class, "numberField1")).thenReturn("An integer");
        when(javadocReader.resolveFieldComment(PrimitiveTypes.class, "numberField2")).thenReturn("A decimal");
        when(javadocReader.resolveFieldComment(ComposedTypes.class, "arrayField")).thenReturn("An array");

        ConstraintReader constraintReader = mock(ConstraintReader.class);

        FieldDocumentationGenerator generator = new FieldDocumentationGenerator(mapper.writer(), javadocReader,
                constraintReader);
        Type type = ComposedTypes.class;

        // when
        List<ExtendedFieldDescriptor> fieldDescriptions = cast(
                generator.generateDocumentation(type, mapper.getTypeFactory()));
        // then
        assertThat(fieldDescriptions.size(), is(10));
        assertThat(fieldDescriptions.get(0), is(descriptor("objectField", "Object", "An object", "true")));
        assertThat(fieldDescriptions.get(1),
                is(descriptor("objectField.stringField", "String", "A string", "true")));
        assertThat(fieldDescriptions.get(2),
                is(descriptor("objectField.booleanField", "Boolean", "A boolean", "true")));
        assertThat(fieldDescriptions.get(3),
                is(descriptor("objectField.numberField1", "Integer", "An integer", "true")));
        assertThat(fieldDescriptions.get(4),
                is(descriptor("objectField.numberField2", "Decimal", "A decimal", "true")));
        assertThat(fieldDescriptions.get(5), is(descriptor("arrayField", "Array", "An array", "true")));
        assertThat(fieldDescriptions.get(6),
                is(descriptor("arrayField[].stringField", "String", "A string", "true")));
        assertThat(fieldDescriptions.get(7),
                is(descriptor("arrayField[].booleanField", "Boolean", "A boolean", "true")));
        assertThat(fieldDescriptions.get(8),
                is(descriptor("arrayField[].numberField1", "Integer", "An integer", "true")));
        assertThat(fieldDescriptions.get(9),
                is(descriptor("arrayField[].numberField2", "Decimal", "A decimal", "true")));
    }

    @Test
    public void testGenerateDocumentationForNestedTypes() throws Exception {
        // given
        ObjectMapper mapper = createMapper();
        JavadocReader javadocReader = mock(JavadocReader.class);
        when(javadocReader.resolveFieldComment(FirstLevel.class, "second")).thenReturn("2nd level");
        when(javadocReader.resolveFieldComment(SecondLevel.class, "third")).thenReturn("3rd level");
        when(javadocReader.resolveFieldComment(ThirdLevel.class, "fourth")).thenReturn("4th level");
        when(javadocReader.resolveFieldComment(FourthLevel.class, "fifth")).thenReturn("5th level");
        when(javadocReader.resolveFieldComment(FifthLevel.class, "last")).thenReturn("An integer");

        ConstraintReader constraintReader = mock(ConstraintReader.class);

        FieldDocumentationGenerator generator = new FieldDocumentationGenerator(mapper.writer(), javadocReader,
                constraintReader);
        Type type = FirstLevel.class;

        // when
        List<ExtendedFieldDescriptor> fieldDescriptions = cast(
                generator.generateDocumentation(type, mapper.getTypeFactory()));

        // then
        assertThat(fieldDescriptions.size(), is(5));
        assertThat(fieldDescriptions.get(0), is(descriptor("second", "Object", "2nd level", "true")));
        assertThat(fieldDescriptions.get(1), is(descriptor("second.third", "Array", "3rd level", "true")));
        assertThat(fieldDescriptions.get(2),
                is(descriptor("second.third[].fourth", "Object", "4th level", "true")));
        assertThat(fieldDescriptions.get(3),
                is(descriptor("second.third[].fourth.fifth", "Array", "5th level", "true")));
        assertThat(fieldDescriptions.get(4),
                is(descriptor("second.third[].fourth.fifth[].last", "Integer", "An integer", "true")));
    }

    @Test
    public void testGenerateDocumentationForRecursiveTypes() throws Exception {
        // given
        ObjectMapper mapper = createMapper();
        JavadocReader javadocReader = mock(JavadocReader.class);
        when(javadocReader.resolveFieldComment(RecursiveType.class, "value")).thenReturn("Type value");
        when(javadocReader.resolveFieldComment(RecursiveType.class, "children")).thenReturn("Child types");
        when(javadocReader.resolveFieldComment(RecursiveType.class, "sibling")).thenReturn("Sibling type");

        ConstraintReader constraintReader = mock(ConstraintReader.class);

        FieldDocumentationGenerator generator = new FieldDocumentationGenerator(mapper.writer(), javadocReader,
                constraintReader);
        Type type = RecursiveType.class;

        // when
        List<ExtendedFieldDescriptor> fieldDescriptions = cast(
                generator.generateDocumentation(type, mapper.getTypeFactory()));

        // then
        assertThat(fieldDescriptions.size(), is(3));
        assertThat(fieldDescriptions.get(0), is(descriptor("value", "String", "Type value", "true")));
        assertThat(fieldDescriptions.get(1), is(descriptor("children", "Array", "Child types", "true")));
        assertThat(fieldDescriptions.get(2), is(descriptor("sibling", "Object", "Sibling type", "true")));
    }

    @Test
    public void testGenerateDocumentationForExternalSerializer() throws Exception {
        // given
        ObjectMapper mapper = createMapper();

        JavadocReader javadocReader = mock(JavadocReader.class);
        when(javadocReader.resolveFieldComment(ExternalSerializer.class, "bigDecimal")).thenReturn("A decimal");

        ConstraintReader constraintReader = mock(ConstraintReader.class);

        SimpleModule testModule = new SimpleModule("TestModule");
        testModule.addSerializer(new BigDecimalSerializer());
        mapper.registerModule(testModule);

        FieldDocumentationGenerator generator = new FieldDocumentationGenerator(mapper.writer(), javadocReader,
                constraintReader);
        Type type = ExternalSerializer.class;

        // when
        List<ExtendedFieldDescriptor> fieldDescriptions = cast(
                generator.generateDocumentation(type, mapper.getTypeFactory()));

        // then
        assertThat(fieldDescriptions.size(), is(1));
        assertThat(fieldDescriptions.get(0), is(descriptor("bigDecimal", "Decimal", "A decimal", "true")));
    }

    @Test
    public void testGenerateDocumentationForJacksonAnnotations() throws Exception {
        // given
        ObjectMapper mapper = createMapper();

        JavadocReader javadocReader = mock(JavadocReader.class);
        when(javadocReader.resolveFieldComment(JsonAnnotations.class, "location")).thenReturn("A location");
        when(javadocReader.resolveFieldComment(JsonAnnotations.class, "uri")).thenReturn("A uri");
        when(javadocReader.resolveMethodComment(JsonAnnotations.class, "getParameter")).thenReturn("A parameter");
        when(javadocReader.resolveFieldComment(JsonAnnotations.Meta.class, "headers")).thenReturn("A header map");

        ConstraintReader constraintReader = mock(ConstraintReader.class);

        FieldDocumentationGenerator generator = new FieldDocumentationGenerator(mapper.writer(), javadocReader,
                constraintReader);
        Type type = JsonAnnotations.class;

        // when
        List<ExtendedFieldDescriptor> fieldDescriptions = cast(
                generator.generateDocumentation(type, mapper.getTypeFactory()));

        // then
        assertThat(fieldDescriptions.size(), is(4));
        // @JsonPropertyOrder puts it to first place
        assertThat(fieldDescriptions.get(0), is(descriptor("uri", "String", "A uri", "true")));
        // @JsonProperty
        assertThat(fieldDescriptions.get(1), is(descriptor("path", "String", "A location", "true")));
        // @JsonGetter
        assertThat(fieldDescriptions.get(2), is(descriptor("param", "String", "A parameter", "true")));
        // @JsonUnwrapped
        assertThat(fieldDescriptions.get(3), is(descriptor("headers", "Map", "A header map", "true")));
    }

    @Test
    public void testGenerateDocumentationForFieldResolution() throws Exception {
        // given
        // different mapper for custom field resolution
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()
                .withFieldVisibility(JsonAutoDetect.Visibility.PUBLIC_ONLY));

        ConstraintReader constraintReader = mock(ConstraintReader.class);

        JavadocReader javadocReader = mock(JavadocReader.class);
        // comment on field directly
        when(javadocReader.resolveFieldComment(FieldCommentResolution.class, "location")).thenReturn("A location");
        // comment on getter instead of field
        when(javadocReader.resolveMethodComment(FieldCommentResolution.class, "getType")).thenReturn("A type");
        // comment on field instead of getter
        when(javadocReader.resolveFieldComment(FieldCommentResolution.class, "uri")).thenReturn("A uri");
        when(javadocReader.resolveFieldComment(FieldCommentResolution.class, "secured"))
                .thenReturn("A secured flag");

        FieldDocumentationGenerator generator = new FieldDocumentationGenerator(mapper.writer(), javadocReader,
                constraintReader);
        Type type = FieldCommentResolution.class;

        // when
        List<ExtendedFieldDescriptor> fieldDescriptions = cast(
                generator.generateDocumentation(type, mapper.getTypeFactory()));

        // then
        assertThat(fieldDescriptions.size(), is(4));
        // field comment
        assertThat(fieldDescriptions.get(0), is(descriptor("location", "String", "A location", "true")));
        // getter comment
        assertThat(fieldDescriptions.get(1), is(descriptor("type", "String", "A type", "true")));
        // field comment
        assertThat(fieldDescriptions.get(2), is(descriptor("uri", "String", "A uri", "true")));
        assertThat(fieldDescriptions.get(3), is(descriptor("secured", "Boolean", "A secured flag", "true")));
    }

    @Test
    public void testGenerateDocumentationForConstraints() throws Exception {
        // given
        ObjectMapper mapper = createMapper();

        JavadocReader javadocReader = mock(JavadocReader.class);

        ConstraintReader constraintReader = mock(ConstraintReader.class);
        when(constraintReader.isMandatory(NotNull.class)).thenReturn(true);
        when(constraintReader.isMandatory(NotEmpty.class)).thenReturn(true);
        when(constraintReader.isMandatory(NotBlank.class)).thenReturn(true);

        when(constraintReader.getConstraintMessages(ConstraintResolution.class, "location"))
                .thenReturn(singletonList("A constraint for location"));
        when(constraintReader.getConstraintMessages(ConstraintResolution.class, "type"))
                .thenReturn(singletonList("A constraint for type"));
        when(constraintReader.getOptionalMessages(ConstraintResolution.class, "type"))
                .thenReturn(singletonList("false"));
        when(constraintReader.getOptionalMessages(ConstraintResolution.class, "params"))
                .thenReturn(singletonList("false"));
        when(constraintReader.getConstraintMessages(ConstraintField.class, "value"))
                .thenReturn(asList("A constraint1 for value", "A constraint2 for value"));
        when(constraintReader.getOptionalMessages(ConstraintField.class, "value"))
                .thenReturn(singletonList("false"));

        FieldDocumentationGenerator generator = new FieldDocumentationGenerator(mapper.writer(), javadocReader,
                constraintReader);
        Type type = ConstraintResolution.class;

        // when
        List<ExtendedFieldDescriptor> fieldDescriptions = cast(
                generator.generateDocumentation(type, mapper.getTypeFactory()));

        // then
        assertThat(fieldDescriptions.size(), is(5));
        assertThat(fieldDescriptions.get(0),
                is(descriptor("location", "String", null, "true", "A constraint for location")));
        assertThat(fieldDescriptions.get(1),
                is(descriptor("type", "Integer", null, "false", "A constraint for type")));
        assertThat(fieldDescriptions.get(2), is(descriptor("params", "Array", null, "false")));
        assertThat(fieldDescriptions.get(3), is(descriptor("params[].value", "String", null, "false",
                "A constraint1 for value", "A constraint2 for value")));
        assertThat(fieldDescriptions.get(4), is(descriptor("flags", "Array", null, "true")));
    }

    private ObjectMapper createMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()
                .withFieldVisibility(JsonAutoDetect.Visibility.ANY));
        return mapper;
    }

    private ExtendedFieldDescriptor descriptor(String path, Object fieldType, String comment, String optional,
            String... constraints) {
        FieldDescriptor fieldDescriptor = fieldWithPath(path).type(fieldType).description(comment);
        fieldDescriptor.attributes(new Attribute(OPTIONAL_ATTRIBUTE, Arrays.asList(optional)));
        if (constraints != null) {
            fieldDescriptor.attributes(new Attribute(CONSTRAINTS_ATTRIBUTE, Arrays.asList(constraints)));
        }
        return new ExtendedFieldDescriptor(fieldDescriptor);
    }

    private List<ExtendedFieldDescriptor> cast(List<FieldDescriptor> original) {
        List<ExtendedFieldDescriptor> casted = new ArrayList<>(original.size());
        for (FieldDescriptor d : original) {
            casted.add(new ExtendedFieldDescriptor(d));
        }
        return casted;
    }

    private static class PrimitiveTypes {
        private String stringField;
        private Boolean booleanField;
        private Integer numberField1;
        private Double numberField2;
    }

    private static class ComposedTypes {
        private PrimitiveTypes objectField;
        private List<PrimitiveTypes> arrayField;
    }

    private static class FirstLevel {
        private SecondLevel second;
    }

    private static class SecondLevel {
        private List<ThirdLevel> third;
    }

    private static class ThirdLevel {
        private FourthLevel fourth;
    }

    private static class FourthLevel {
        private List<FifthLevel> fifth;
    }

    private static class FifthLevel {
        private Integer last;
    }

    private static class ExternalSerializer {
        private BigDecimal bigDecimal;
    }

    @JsonInclude(JsonInclude.Include.NON_NULL) // does not affect field resolution
    @JsonIgnoreProperties({ "value" })
    @JsonPropertyOrder({ "uri", "path" })
    private static class JsonAnnotations {
        @JsonProperty("path")
        private String location;

        @JsonIgnore
        private String type;

        private String value;

        private String uri;

        private String param;

        @JsonUnwrapped
        private Meta meta;

        @JsonGetter("param")
        public String getParameter() {
            return param;
        }

        private class Meta {
            private Map<String, String> headers;
        }
    }

    private static class FieldCommentResolution {
        public String location; // doc is here
        public String type;
        private String uri; // doc is here
        private boolean secured;// doc is here

        public String getType() { // doc is here
            return type;
        }

        public String getUri() {
            return uri;
        }

        public boolean isSecured() {
            return secured;
        }
    }

    private static class ConstraintResolution {
        @Length(min = 1, max = 255)
        private String location;
        @NotNull
        @Min(1)
        @Max(10)
        private Integer type;
        @Valid
        @NotEmpty
        private List<ConstraintField> params;
        private List<Boolean> flags;
    }

    private static class ConstraintField {
        @NotBlank
        @Size(max = 20)
        private String value;
    }

    private static class RecursiveType {
        private String value;
        @RestdocsNotExpanded
        private List<RecursiveType> children;
        @RestdocsNotExpanded
        private RecursiveType sibling;
    }
}