From ada273ba9eadb00705263fba0489086a965076b1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 21 Nov 2018 13:03:24 +0000 Subject: [PATCH] Rework JsonFieldTypeResolver to only discover the field types See gh-549 --- .../restdocs/payload/JsonContentHandler.java | 12 +- .../restdocs/payload/JsonFieldProcessor.java | 17 +- .../restdocs/payload/JsonFieldTypes.java | 57 ++++ ...ver.java => JsonFieldTypesDiscoverer.java} | 56 ++-- .../payload/JsonFieldProcessorTests.java | 39 ++- .../payload/JsonFieldTypeResolverTests.java | 263 ------------------ .../JsonFieldTypesDiscovererTests.java | 209 ++++++++++++++ .../restdocs/payload/JsonFieldTypesTests.java | 63 +++++ 8 files changed, 402 insertions(+), 314 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypes.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/{JsonFieldTypeResolver.java => JsonFieldTypesDiscoverer.java} (58%) delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index 1fbc2b5f..1ea88462 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -37,7 +37,7 @@ class JsonContentHandler implements ContentHandler { private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); - private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver(); + private final JsonFieldTypesDiscoverer fieldTypesDiscoverer = new JsonFieldTypesDiscoverer(); private final ObjectMapper objectMapper = new ObjectMapper() .enable(SerializationFeature.INDENT_OUTPUT); @@ -147,16 +147,18 @@ class JsonContentHandler implements ContentHandler { @Override public Object determineFieldType(FieldDescriptor fieldDescriptor) { if (fieldDescriptor.getType() == null) { - return this.fieldTypeResolver.resolveFieldType(fieldDescriptor, - readContent()); + return this.fieldTypesDiscoverer + .discoverFieldTypes(fieldDescriptor.getPath(), readContent()) + .coalesce(fieldDescriptor.isOptional()); } if (!(fieldDescriptor.getType() instanceof JsonFieldType)) { return fieldDescriptor.getType(); } JsonFieldType descriptorFieldType = (JsonFieldType) fieldDescriptor.getType(); try { - JsonFieldType actualFieldType = this.fieldTypeResolver - .resolveFieldType(fieldDescriptor, readContent()); + JsonFieldType actualFieldType = this.fieldTypesDiscoverer + .discoverFieldTypes(fieldDescriptor.getPath(), readContent()) + .coalesce(fieldDescriptor.isOptional()); if (descriptorFieldType == JsonFieldType.VARIES || descriptorFieldType == actualFieldType || (fieldDescriptor.isOptional() diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index 74b1f6a6..9f50fcc0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -41,20 +41,25 @@ final class JsonFieldProcessor { ExtractedField extract(String path, Object payload) { JsonFieldPath compiledPath = JsonFieldPath.compile(path); - final List matches = new ArrayList<>(); + final List values = new ArrayList<>(); traverse(new ProcessingContext(payload, compiledPath), new MatchCallback() { @Override public void foundMatch(Match match) { - matches.add(match.getValue()); + values.add(match.getValue()); + } + + @Override + public void absent() { + values.add(ExtractedField.ABSENT); } }); - if (matches.isEmpty()) { - throw new FieldDoesNotExistException(path); + if (values.isEmpty()) { + values.add(ExtractedField.ABSENT); } return new ExtractedField( - (compiledPath.getType() != PathType.SINGLE) ? matches : matches.get(0), + (compiledPath.getType() != PathType.SINGLE) ? values : values.get(0), compiledPath.getType()); } @@ -427,6 +432,8 @@ final class JsonFieldProcessor { */ static class ExtractedField { + static final Object ABSENT = new Object(); + private final Object value; private final PathType type; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypes.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypes.java new file mode 100644 index 00000000..7584b12b --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypes.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2018 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 org.springframework.restdocs.payload; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * {@link JsonFieldType Types} for a field discovered in a JSON payload. + * + * @author Andy Wilkinson + */ +class JsonFieldTypes implements Iterable { + + private final Set fieldTypes; + + JsonFieldTypes(JsonFieldType fieldType) { + this(Collections.singleton(fieldType)); + } + + JsonFieldTypes(Set fieldTypes) { + this.fieldTypes = fieldTypes; + } + + JsonFieldType coalesce(boolean optional) { + Set types = new HashSet<>(this.fieldTypes); + if (optional && types.size() > 1) { + types.remove(JsonFieldType.NULL); + } + if (types.size() == 1) { + return types.iterator().next(); + } + return JsonFieldType.VARIES; + } + + @Override + public Iterator iterator() { + return this.fieldTypes.iterator(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypesDiscoverer.java similarity index 58% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypesDiscoverer.java index a577bc65..60749833 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypesDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2017 the original author or authors. + * Copyright 2014-2018 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. @@ -17,50 +17,45 @@ package org.springframework.restdocs.payload; import java.util.Collection; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.springframework.restdocs.payload.JsonFieldPath.PathType; import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; /** - * Resolves the type of a field in a JSON request or response payload. + * Discovers the types of the fields found at a path in a JSON request or response + * payload. * * @author Andy Wilkinson */ -class JsonFieldTypeResolver { +class JsonFieldTypesDiscoverer { private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); - JsonFieldType resolveFieldType(FieldDescriptor fieldDescriptor, Object payload) { - ExtractedField extractedField = this.fieldProcessor - .extract(fieldDescriptor.getPath(), payload); + JsonFieldTypes discoverFieldTypes(String path, Object payload) { + ExtractedField extractedField = this.fieldProcessor.extract(path, payload); Object value = extractedField.getValue(); if (value instanceof Collection && extractedField.getType() == PathType.MULTI) { - JsonFieldType commonType = null; - for (Object item : (Collection) value) { - JsonFieldType fieldType = determineFieldType(item); - if (commonType == null) { - commonType = fieldType; - } - else if (fieldType != commonType) { - if (!fieldDescriptor.isOptional()) { - return JsonFieldType.VARIES; - } - if (commonType == JsonFieldType.NULL) { - commonType = fieldType; - } - else if (fieldType != JsonFieldType.NULL) { - return JsonFieldType.VARIES; - } - } + Collection values = (Collection) value; + if (allAbsent(values)) { + throw new FieldDoesNotExistException(path); } - return commonType; + Set fieldTypes = new HashSet<>(); + for (Object item : values) { + fieldTypes.add(determineFieldType(item)); + } + return new JsonFieldTypes(fieldTypes); } - return determineFieldType(value); + if (value == ExtractedField.ABSENT) { + throw new FieldDoesNotExistException(path); + } + return new JsonFieldTypes(determineFieldType(value)); } private JsonFieldType determineFieldType(Object fieldValue) { - if (fieldValue == null) { + if (fieldValue == null || fieldValue == ExtractedField.ABSENT) { return JsonFieldType.NULL; } if (fieldValue instanceof String) { @@ -78,4 +73,13 @@ class JsonFieldTypeResolver { return JsonFieldType.NUMBER; } + private boolean allAbsent(Collection values) { + for (Object value : values) { + if (value != ExtractedField.ABSENT) { + return false; + } + } + return true; + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index c8733b81..a8632d49 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -28,6 +28,8 @@ import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Test; +import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -109,7 +111,7 @@ public class JsonFieldProcessorTests { new HashMap()); payload.put("a", alpha); assertThat(this.fieldProcessor.extract("a[].b", payload).getValue()) - .isEqualTo(Arrays.asList("bravo")); + .isEqualTo(Arrays.asList("bravo", ExtractedField.ABSENT)); } @Test @@ -165,54 +167,61 @@ public class JsonFieldProcessorTests { Arrays.asList(4))); } - @Test(expected = FieldDoesNotExistException.class) + @Test public void nonExistentTopLevelField() { - this.fieldProcessor.extract("a", Collections.emptyMap()); + assertThat(this.fieldProcessor.extract("a", Collections.emptyMap()).getValue()) + .isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) + @Test public void nonExistentNestedField() { HashMap payload = new HashMap<>(); payload.put("a", new HashMap()); - this.fieldProcessor.extract("a.b", payload); + assertThat(this.fieldProcessor.extract("a.b", payload).getValue()) + .isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) + @Test public void nonExistentNestedFieldWhenParentIsNotAMap() { HashMap payload = new HashMap<>(); payload.put("a", 5); - this.fieldProcessor.extract("a.b", payload); + assertThat(this.fieldProcessor.extract("a.b", payload).getValue()) + .isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) + @Test public void nonExistentFieldWhenParentIsAnArray() { HashMap payload = new HashMap<>(); HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); payload.put("a", alpha); - this.fieldProcessor.extract("a.b.c", payload); + assertThat(this.fieldProcessor.extract("a.b.c", payload).getValue()) + .isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) + @Test public void nonExistentArrayField() { HashMap payload = new HashMap<>(); - this.fieldProcessor.extract("a[]", payload); + assertThat(this.fieldProcessor.extract("a[]", payload).getValue()) + .isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) + @Test public void nonExistentArrayFieldAsTypeDoesNotMatch() { HashMap payload = new HashMap<>(); payload.put("a", 5); - this.fieldProcessor.extract("a[]", payload); + assertThat(this.fieldProcessor.extract("a[]", payload).getValue()) + .isEqualTo(ExtractedField.ABSENT); } - @Test(expected = FieldDoesNotExistException.class) + @Test public void nonExistentFieldBeneathAnArray() { HashMap payload = new HashMap<>(); HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); payload.put("a", alpha); - this.fieldProcessor.extract("a.b[].id", payload); + assertThat(this.fieldProcessor.extract("a.b[].id", payload).getValue()) + .isEqualTo(Arrays.asList(ExtractedField.ABSENT)); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java deleted file mode 100644 index 3ebb74cf..00000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright 2014-2018 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 org.springframework.restdocs.payload; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link JsonFieldTypeResolver}. - * - * @author Andy Wilkinson - */ -public class JsonFieldTypeResolverTests { - - private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver(); - - @Rule - public ExpectedException thrownException = ExpectedException.none(); - - @Test - public void arrayField() throws IOException { - assertFieldType(JsonFieldType.ARRAY, "[]"); - } - - @Test - public void topLevelArray() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("[]"), - new ObjectMapper().readValue("[{\"a\":\"alpha\"}]", List.class))) - .isEqualTo(JsonFieldType.ARRAY); - } - - @Test - public void nestedArray() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[]"), - createPayload("{\"a\": [{\"b\":\"bravo\"}]}"))) - .isEqualTo(JsonFieldType.ARRAY); - } - - @Test - public void arrayNestedBeneathAnArray() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].b[]"), - createPayload("{\"a\": [{\"b\": [ 1, 2 ]}]}"))) - .isEqualTo(JsonFieldType.ARRAY); - } - - @Test - public void specificFieldOfObjectInArrayNestedBeneathAnArray() throws IOException { - assertThat( - this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].b[].c"), - createPayload("{\"a\": [{\"b\": [ {\"c\": 5}, {\"c\": 5}]}]}"))) - .isEqualTo(JsonFieldType.NUMBER); - } - - @Test - public void booleanField() throws IOException { - assertFieldType(JsonFieldType.BOOLEAN, "true"); - } - - @Test - public void objectField() throws IOException { - assertFieldType(JsonFieldType.OBJECT, "{}"); - } - - @Test - public void nullField() throws IOException { - assertFieldType(JsonFieldType.NULL, "null"); - } - - @Test - public void numberField() throws IOException { - assertFieldType(JsonFieldType.NUMBER, "1.2345"); - } - - @Test - public void stringField() throws IOException { - assertFieldType(JsonFieldType.STRING, "\"Foo\""); - } - - @Test - public void nestedField() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a.b.c"), - createPayload("{\"a\":{\"b\":{\"c\":{}}}}"))) - .isEqualTo(JsonFieldType.OBJECT); - } - - @Test - public void multipleFieldsWithSameType() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].id"), - createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}"))) - .isEqualTo(JsonFieldType.NUMBER); - } - - @Test - public void multipleFieldsWithDifferentTypes() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].id"), - createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}"))) - .isEqualTo(JsonFieldType.VARIES); - } - - @Test - public void multipleFieldsWithDifferentTypesAndSometimesAbsent() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].id"), - createPayload("{\"a\":[{\"id\":1},{\"id\":true}, { }]}"))) - .isEqualTo(JsonFieldType.VARIES); - } - - @Test - public void multipleFieldsWithDifferentTypesAndSometimesAbsentWhenOptionalResolvesToVaries() - throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType( - new FieldDescriptor("a[].id").optional(), - createPayload("{\"a\":[{\"id\":1},{\"id\":true}, { }]}"))) - .isEqualTo(JsonFieldType.VARIES); - } - - @Test - public void multipleFieldsWhenSometimesAbsent() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].id"), - createPayload("{\"a\":[{\"id\":1},{ }]}"))) - .isEqualTo(JsonFieldType.NUMBER); - } - - @Test - public void multipleFieldsWithDifferentTypesAndSometimesNull() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].id"), - createPayload("{\"a\":[{\"id\":1},{\"id\":true}, {\"id\":null}]}"))) - .isEqualTo(JsonFieldType.VARIES); - } - - @Test - public void multipleFieldsWhenNotNullThenNullWhenRequiredHasVariesType() - throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].id"), - createPayload("{\"a\":[{\"id\":1},{\"id\":null}]}"))) - .isEqualTo(JsonFieldType.VARIES); - } - - @Test - public void multipleFieldsWhenNotNullThenNullWhenOptionalHasSpecificType() - throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType( - new FieldDescriptor("a[].id").optional(), - createPayload("{\"a\":[{\"id\":1},{\"id\":null}]}"))) - .isEqualTo(JsonFieldType.NUMBER); - } - - @Test - public void multipleFieldsWhenNullThenNotNullWhenRequiredHasVariesType() - throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].id"), - createPayload("{\"a\":[{\"id\":null},{\"id\":1}]}"))) - .isEqualTo(JsonFieldType.VARIES); - } - - @Test - public void multipleFieldsWhenNullThenNotNullWhenOptionalHasSpecificType() - throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType( - new FieldDescriptor("a[].id").optional(), - createPayload("{\"a\":[{\"id\":null},{\"id\":1}]}"))) - .isEqualTo(JsonFieldType.NUMBER); - } - - @Test - public void multipleFieldsWhenEitherNullOrAbsent() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].id"), - createPayload("{\"a\":[{},{\"id\":null}]}"))) - .isEqualTo(JsonFieldType.NULL); - } - - @Test - public void multipleFieldsThatAreAllNull() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].id"), - createPayload("{\"a\":[{\"id\":null},{\"id\":null}]}"))) - .isEqualTo(JsonFieldType.NULL); - } - - @Test - public void nonExistentSingleFieldProducesFieldDoesNotExistException() - throws IOException { - this.thrownException.expect(FieldDoesNotExistException.class); - this.thrownException.expectMessage( - "The payload does not contain a field with the path 'a.b'"); - this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a.b"), - createPayload("{\"a\":{}}")); - } - - @Test - public void nonExistentMultipleFieldsProducesFieldDoesNotExistException() - throws IOException { - this.thrownException.expect(FieldDoesNotExistException.class); - this.thrownException.expectMessage( - "The payload does not contain a field with the path 'a[].b'"); - this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a[].b"), - createPayload("{\"a\":[{\"c\":1},{\"c\":2}]}")); - } - - @Test - public void leafWildcardWithCommonType() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a.*"), - createPayload("{\"a\": {\"b\": 5, \"c\": 6}}"))) - .isEqualTo(JsonFieldType.NUMBER); - } - - @Test - public void leafWildcardWithVaryingType() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a.*"), - createPayload("{\"a\": {\"b\": 5, \"c\": \"six\"}}"))) - .isEqualTo(JsonFieldType.VARIES); - } - - @Test - public void intermediateWildcardWithCommonType() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a.*.d"), - createPayload("{\"a\": {\"b\": {\"d\": 4}, \"c\": {\"d\": 5}}}}"))) - .isEqualTo(JsonFieldType.NUMBER); - } - - @Test - public void intermediateWildcardWithVaryingType() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("a.*.d"), - createPayload("{\"a\": {\"b\": {\"d\": 4}, \"c\": {\"d\": \"four\"}}}}"))) - .isEqualTo(JsonFieldType.VARIES); - } - - private void assertFieldType(JsonFieldType expectedType, String jsonValue) - throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType(new FieldDescriptor("field"), - createSimplePayload(jsonValue))).isEqualTo(expectedType); - } - - private Map createSimplePayload(String value) throws IOException { - return createPayload("{\"field\":" + value + "}"); - } - - @SuppressWarnings("unchecked") - private Map createPayload(String json) throws IOException { - return new ObjectMapper().readValue(json, Map.class); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java new file mode 100644 index 00000000..1d05fac6 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesDiscovererTests.java @@ -0,0 +1,209 @@ +/* + * Copyright 2014-2018 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 org.springframework.restdocs.payload; + +import java.io.IOException; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonFieldTypesDiscoverer}. + * + * @author Andy Wilkinson + */ +public class JsonFieldTypesDiscovererTests { + + private final JsonFieldTypesDiscoverer fieldTypeDiscoverer = new JsonFieldTypesDiscoverer(); + + @Rule + public ExpectedException thrownException = ExpectedException.none(); + + @Test + public void arrayField() throws IOException { + assertThat(discoverFieldTypes("[]")).containsExactly(JsonFieldType.ARRAY); + } + + @Test + public void topLevelArray() throws IOException { + assertThat(discoverFieldTypes("[]", "[{\"a\":\"alpha\"}]")) + .containsExactly(JsonFieldType.ARRAY); + } + + @Test + public void nestedArray() throws IOException { + assertThat(discoverFieldTypes("a[]", "{\"a\": [{\"b\":\"bravo\"}]}")) + .containsExactly(JsonFieldType.ARRAY); + } + + @Test + public void arrayNestedBeneathAnArray() throws IOException { + assertThat(discoverFieldTypes("a[].b[]", "{\"a\": [{\"b\": [ 1, 2 ]}]}")) + .containsExactly(JsonFieldType.ARRAY); + } + + @Test + public void specificFieldOfObjectInArrayNestedBeneathAnArray() throws IOException { + assertThat(discoverFieldTypes("a[].b[].c", + "{\"a\": [{\"b\": [ {\"c\": 5}, {\"c\": 5}]}]}")) + .containsExactly(JsonFieldType.NUMBER); + } + + @Test + public void booleanField() throws IOException { + assertThat(discoverFieldTypes("true")).containsExactly(JsonFieldType.BOOLEAN); + } + + @Test + public void objectField() throws IOException { + assertThat(discoverFieldTypes("{}")).containsExactly(JsonFieldType.OBJECT); + } + + @Test + public void nullField() throws IOException { + assertThat(discoverFieldTypes("null")).containsExactly(JsonFieldType.NULL); + } + + @Test + public void numberField() throws IOException { + assertThat(discoverFieldTypes("1.2345")).containsExactly(JsonFieldType.NUMBER); + } + + @Test + public void stringField() throws IOException { + assertThat(discoverFieldTypes("\"Foo\"")).containsExactly(JsonFieldType.STRING); + } + + @Test + public void nestedField() throws IOException { + assertThat(discoverFieldTypes("a.b.c", "{\"a\":{\"b\":{\"c\":{}}}}")) + .containsExactly(JsonFieldType.OBJECT); + } + + @Test + public void multipleFieldsWithSameType() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":2}]}")) + .containsExactly(JsonFieldType.NUMBER); + } + + @Test + public void multipleFieldsWithDifferentTypes() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":true}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.BOOLEAN); + } + + @Test + public void multipleFieldsWithDifferentTypesAndSometimesAbsent() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":true}, {}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.BOOLEAN, + JsonFieldType.NULL); + } + + @Test + public void multipleFieldsWhenSometimesAbsent() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":1},{\"id\":2}, {}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.NULL); + } + + @Test + public void multipleFieldsWhenSometimesNull() throws IOException { + assertThat(discoverFieldTypes("a[].id", + "{\"a\":[{\"id\":1},{\"id\":2}, {\"id\":null}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, + JsonFieldType.NULL); + } + + @Test + public void multipleFieldsWithDifferentTypesAndSometimesNull() throws IOException { + assertThat(discoverFieldTypes("a[].id", + "{\"a\":[{\"id\":1},{\"id\":true}, {\"id\":null}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, + JsonFieldType.BOOLEAN, JsonFieldType.NULL); + } + + @Test + public void multipleFieldsWhenEitherNullOrAbsent() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{},{\"id\":null}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NULL); + } + + @Test + public void multipleFieldsThatAreAllNull() throws IOException { + assertThat(discoverFieldTypes("a[].id", "{\"a\":[{\"id\":null},{\"id\":null}]}")) + .containsExactlyInAnyOrder(JsonFieldType.NULL); + } + + @Test + public void nonExistentSingleFieldProducesFieldDoesNotExistException() + throws IOException { + this.thrownException.expect(FieldDoesNotExistException.class); + this.thrownException.expectMessage( + "The payload does not contain a field with the path 'a.b'"); + discoverFieldTypes("a.b", "{\"a\":{}}"); + } + + @Test + public void nonExistentMultipleFieldsProducesFieldDoesNotExistException() + throws IOException { + this.thrownException.expect(FieldDoesNotExistException.class); + this.thrownException.expectMessage( + "The payload does not contain a field with the path 'a[].b'"); + discoverFieldTypes("a[].b", "{\"a\":[{\"c\":1},{\"c\":2}]}"); + } + + @Test + public void leafWildcardWithCommonType() throws IOException { + assertThat(discoverFieldTypes("a.*", "{\"a\": {\"b\": 5, \"c\": 6}}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER); + } + + @Test + public void leafWildcardWithVaryingType() throws IOException { + assertThat(discoverFieldTypes("a.*", "{\"a\": {\"b\": 5, \"c\": \"six\"}}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, JsonFieldType.STRING); + } + + @Test + public void intermediateWildcardWithCommonType() throws IOException { + assertThat(discoverFieldTypes("a.*.d", + "{\"a\": {\"b\": {\"d\": 4}, \"c\": {\"d\": 5}}}}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER); + } + + @Test + public void intermediateWildcardWithVaryingType() throws IOException { + assertThat(discoverFieldTypes("a.*.d", + "{\"a\": {\"b\": {\"d\": 4}, \"c\": {\"d\": \"four\"}}}}")) + .containsExactlyInAnyOrder(JsonFieldType.NUMBER, + JsonFieldType.STRING); + } + + private JsonFieldTypes discoverFieldTypes(String value) throws IOException { + return discoverFieldTypes("field", "{\"field\":" + value + "}"); + } + + private JsonFieldTypes discoverFieldTypes(String path, String json) + throws IOException { + return this.fieldTypeDiscoverer.discoverFieldTypes(path, + new ObjectMapper().readValue(json, Object.class)); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java new file mode 100644 index 00000000..01d4b5e5 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypesTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2014-2018 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 org.springframework.restdocs.payload; + +import java.util.EnumSet; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonFieldTypes}. + * + * @author Andy Wilkinson + */ +public class JsonFieldTypesTests { + + @Test + public void singleTypeCoalescesToThatType() { + assertThat(new JsonFieldTypes(JsonFieldType.NUMBER).coalesce(false)) + .isEqualTo(JsonFieldType.NUMBER); + } + + @Test + public void singleTypeCoalescesToThatTypeWhenOptional() { + assertThat(new JsonFieldTypes(JsonFieldType.NUMBER).coalesce(true)) + .isEqualTo(JsonFieldType.NUMBER); + } + + @Test + public void multipleTypesCoalescesToVaries() { + assertThat( + new JsonFieldTypes(EnumSet.of(JsonFieldType.ARRAY, JsonFieldType.NUMBER)) + .coalesce(false)).isEqualTo(JsonFieldType.VARIES); + } + + @Test + public void nullAndNonNullTypesCoalescesToVaries() { + assertThat(new JsonFieldTypes(EnumSet.of(JsonFieldType.ARRAY, JsonFieldType.NULL)) + .coalesce(false)).isEqualTo(JsonFieldType.VARIES); + } + + @Test + public void nullAndNonNullTypesCoalescesToNonNullTypeWhenOptional() { + assertThat(new JsonFieldTypes(EnumSet.of(JsonFieldType.ARRAY, JsonFieldType.NULL)) + .coalesce(true)).isEqualTo(JsonFieldType.ARRAY); + } + +}