diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index fafde4a0..990cd2bb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -38,6 +38,7 @@ import org.springframework.util.StringUtils; * * @author Andreas Evers * @author Andy Wilkinson + * @author Mathias Düsterhöft */ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { @@ -160,7 +161,7 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { content = verifyContent( this.subsectionExtractor.extractSubsection(content, contentType)); } - ContentHandler contentHandler = getContentHandler(content, contentType); + ContentHandler contentHandler = ContentHandler.forContent(content, contentType); validateFieldDocumentation(contentHandler); @@ -168,7 +169,7 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { for (FieldDescriptor descriptor : this.fieldDescriptors) { if (!descriptor.isIgnored()) { try { - Object type = contentHandler.determineFieldType(descriptor); + Object type = contentHandler.resolveFieldType(descriptor); descriptorsToDocument.add(copyWithType(descriptor, type)); } catch (FieldDoesNotExistException ex) { @@ -200,36 +201,6 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { return content; } - private ContentHandler getContentHandler(byte[] content, MediaType contentType) { - ContentHandler contentHandler = createJsonContentHandler(content); - if (contentHandler == null) { - contentHandler = createXmlContentHandler(content); - if (contentHandler == null) { - throw new PayloadHandlingException("Cannot handle " + contentType - + " content as it could not be parsed as JSON or XML"); - } - } - return contentHandler; - } - - private ContentHandler createJsonContentHandler(byte[] content) { - try { - return new JsonContentHandler(content); - } - catch (Exception ex) { - return null; - } - } - - private ContentHandler createXmlContentHandler(byte[] content) { - try { - return new XmlContentHandler(content); - } - catch (Exception ex) { - return null; - } - } - private void validateFieldDocumentation(ContentHandler payloadHandler) { List missingFields = payloadHandler .findMissingFields(this.fieldDescriptors); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java index 32bed30b..9349ee88 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java @@ -18,12 +18,15 @@ package org.springframework.restdocs.payload; import java.util.List; +import org.springframework.http.MediaType; + /** * A handler for the content of a request or response. * * @author Andy Wilkinson + * @author Mathias Düsterhöft */ -interface ContentHandler { +interface ContentHandler extends FieldTypeResolver { /** * Finds the fields that are missing from the handler's payload. A field is missing if @@ -48,11 +51,26 @@ interface ContentHandler { String getUndocumentedContent(List fieldDescriptors); /** - * Returns the type of the field that is described by the given - * {@code fieldDescriptor} based on the content of the payload. - * @param fieldDescriptor the field descriptor - * @return the type of the field + * Create a {@link ContentHandler} for the given content type and payload. + * @param content the payload + * @param contentType the content type + * @return the ContentHandler + * @throws PayloadHandlingException if no known ContentHandler can handle the content */ - Object determineFieldType(FieldDescriptor fieldDescriptor); + static ContentHandler forContent(byte[] content, MediaType contentType) { + + try { + return new JsonContentHandler(content); + } + catch (Exception je) { + try { + return new XmlContentHandler(content); + } + catch (Exception xe) { + throw new PayloadHandlingException("Cannot handle " + contentType + + " content as it could not be parsed as JSON or XML"); + } + } + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java new file mode 100644 index 00000000..32aa4fc0 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java @@ -0,0 +1,49 @@ +/* + * 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 org.springframework.http.MediaType; + +/** + * Resolves the type of a field in a request or response payload. + * + * @author Mathias Düsterhöft + * @author Andy Wilkinson + * @since 2.0.3 + */ +public interface FieldTypeResolver { + + /** + * Create a {@code FieldTypeResolver} for the given {@code content} and + * {@code contentType}. + * @param content the payload that the {@code FieldTypeResolver} should handle + * @param contentType the content type of the payload + * @return the {@code FieldTypeResolver} + */ + static FieldTypeResolver forContent(byte[] content, MediaType contentType) { + return ContentHandler.forContent(content, contentType); + } + + /** + * Resolves the type of the field that is described by the given + * {@code fieldDescriptor} based on the content of the payload. + * @param fieldDescriptor the field descriptor + * @return the type of the field + */ + Object resolveFieldType(FieldDescriptor fieldDescriptor); + +} 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..08875e41 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 @@ -32,12 +32,13 @@ import org.springframework.restdocs.payload.JsonFieldProcessor.ExtractedField; * A {@link ContentHandler} for JSON content. * * @author Andy Wilkinson + * @author Mathias Düsterhöft */ 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); @@ -145,18 +146,20 @@ class JsonContentHandler implements ContentHandler { } @Override - public Object determineFieldType(FieldDescriptor fieldDescriptor) { + public Object resolveFieldType(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/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 45c08846..c82ebb78 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -190,7 +190,7 @@ class XmlContentHandler implements ContentHandler { } @Override - public Object determineFieldType(FieldDescriptor fieldDescriptor) { + public Object resolveFieldType(FieldDescriptor fieldDescriptor) { if (fieldDescriptor.getType() != null) { return fieldDescriptor.getType(); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java new file mode 100644 index 00000000..0ed8780c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java @@ -0,0 +1,55 @@ +/* + * 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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.http.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link FieldTypeResolver}. + * + * @author Mathias Düsterhöft + */ +public class FieldTypeResolverTests { + + @Rule + public ExpectedException thrownException = ExpectedException.none(); + + @Test + public void returnJsonFieldTypeResolver() { + assertThat(FieldTypeResolver.forContent("{\"field\": \"value\"}".getBytes(), + MediaType.APPLICATION_JSON)).isInstanceOf(JsonContentHandler.class); + } + + @Test + public void returnXmlContentHandler() { + assertThat(FieldTypeResolver.forContent("5".getBytes(), + MediaType.APPLICATION_XML)).isInstanceOf(XmlContentHandler.class); + } + + @Test + public void throwOnInvalidContent() { + this.thrownException.expect(PayloadHandlingException.class); + FieldTypeResolver.forContent("some".getBytes(), MediaType.APPLICATION_XML); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java index efe6cc70..67114e0b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonContentHandlerTests.java @@ -29,6 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link JsonContentHandler}. * * @author Andy Wilkinson + * @author Mathias Düsterhöft */ public class JsonContentHandlerTests { @@ -39,14 +40,14 @@ public class JsonContentHandlerTests { public void typeForFieldWithNullValueMustMatch() { this.thrown.expect(FieldTypesDoNotMatchException.class); new JsonContentHandler("{\"a\": null}".getBytes()) - .determineFieldType(new FieldDescriptor("a").type(JsonFieldType.STRING)); + .resolveFieldType(new FieldDescriptor("a").type(JsonFieldType.STRING)); } @Test public void typeForFieldWithNotNullAndThenNullValueMustMatch() { this.thrown.expect(FieldTypesDoNotMatchException.class); new JsonContentHandler("{\"a\":[{\"id\":1},{\"id\":null}]}".getBytes()) - .determineFieldType( + .resolveFieldType( new FieldDescriptor("a[].id").type(JsonFieldType.STRING)); } @@ -54,7 +55,7 @@ public class JsonContentHandlerTests { public void typeForFieldWithNullAndThenNotNullValueMustMatch() { this.thrown.expect(FieldTypesDoNotMatchException.class); new JsonContentHandler("{\"a\":[{\"id\":null},{\"id\":1}]}".getBytes()) - .determineFieldType( + .resolveFieldType( new FieldDescriptor("a.[].id").type(JsonFieldType.STRING)); } @@ -62,7 +63,7 @@ public class JsonContentHandlerTests { public void typeForOptionalFieldWithNumberAndThenNullValueIsNumber() { Object fieldType = new JsonContentHandler( "{\"a\":[{\"id\":1},{\"id\":null}]}\"".getBytes()) - .determineFieldType(new FieldDescriptor("a[].id").optional()); + .resolveFieldType(new FieldDescriptor("a[].id").optional()); assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.NUMBER); } @@ -70,7 +71,7 @@ public class JsonContentHandlerTests { public void typeForOptionalFieldWithNullAndThenNumberIsNumber() { Object fieldType = new JsonContentHandler( "{\"a\":[{\"id\":null},{\"id\":1}]}".getBytes()) - .determineFieldType(new FieldDescriptor("a[].id").optional()); + .resolveFieldType(new FieldDescriptor("a[].id").optional()); assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.NUMBER); } @@ -78,7 +79,7 @@ public class JsonContentHandlerTests { public void typeForFieldWithNumberAndThenNullValueIsVaries() { Object fieldType = new JsonContentHandler( "{\"a\":[{\"id\":1},{\"id\":null}]}\"".getBytes()) - .determineFieldType(new FieldDescriptor("a[].id")); + .resolveFieldType(new FieldDescriptor("a[].id")); assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.VARIES); } @@ -86,14 +87,14 @@ public class JsonContentHandlerTests { public void typeForFieldWithNullAndThenNumberIsVaries() { Object fieldType = new JsonContentHandler( "{\"a\":[{\"id\":null},{\"id\":1}]}".getBytes()) - .determineFieldType(new FieldDescriptor("a[].id")); + .resolveFieldType(new FieldDescriptor("a[].id")); assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.VARIES); } @Test public void typeForOptionalFieldWithNullValueCanBeProvidedExplicitly() { Object fieldType = new JsonContentHandler("{\"a\": null}".getBytes()) - .determineFieldType( + .resolveFieldType( new FieldDescriptor("a").type(JsonFieldType.STRING).optional()); assertThat((JsonFieldType) fieldType).isEqualTo(JsonFieldType.STRING); } 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); + } + +}