Merge pull request #549 from Mathias Düsterhöft

* gh-549:
  Polish "Provide a public API for resolving the type of a field"
  Provide a public API for resolving the type of a field
  Rework JsonFieldTypeResolver to only discover the field types
This commit is contained in:
Andy Wilkinson
2018-11-21 14:28:09 +00:00
14 changed files with 545 additions and 362 deletions

View File

@@ -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<FieldDescriptor> missingFields = payloadHandler
.findMissingFields(this.fieldDescriptors);

View File

@@ -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<FieldDescriptor> 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");
}
}
}
}

View File

@@ -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);
}

View File

@@ -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()

View File

@@ -41,20 +41,25 @@ final class JsonFieldProcessor {
ExtractedField extract(String path, Object payload) {
JsonFieldPath compiledPath = JsonFieldPath.compile(path);
final List<Object> matches = new ArrayList<>();
final List<Object> 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;

View File

@@ -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<JsonFieldType> {
private final Set<JsonFieldType> fieldTypes;
JsonFieldTypes(JsonFieldType fieldType) {
this(Collections.singleton(fieldType));
}
JsonFieldTypes(Set<JsonFieldType> fieldTypes) {
this.fieldTypes = fieldTypes;
}
JsonFieldType coalesce(boolean optional) {
Set<JsonFieldType> 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<JsonFieldType> iterator() {
return this.fieldTypes.iterator();
}
}

View File

@@ -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<JsonFieldType> 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;
}
}

View File

@@ -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();
}

View File

@@ -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("<a><b>5</b></a>".getBytes(),
MediaType.APPLICATION_XML)).isInstanceOf(XmlContentHandler.class);
}
@Test
public void throwOnInvalidContent() {
this.thrownException.expect(PayloadHandlingException.class);
FieldTypeResolver.forContent("some".getBytes(), MediaType.APPLICATION_XML);
}
}

View File

@@ -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);
}

View File

@@ -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<String, Object>());
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<String, Object> payload = new HashMap<>();
payload.put("a", new HashMap<String, Object>());
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<String, Object> 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<String, Object> payload = new HashMap<>();
HashMap<String, Object> alpha = new HashMap<>();
alpha.put("b", Arrays.asList(new HashMap<String, Object>()));
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<String, Object> 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<String, Object> 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<String, Object> payload = new HashMap<>();
HashMap<String, Object> alpha = new HashMap<>();
alpha.put("b", Arrays.asList(new HashMap<String, Object>()));
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

View File

@@ -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<String, Object> createSimplePayload(String value) throws IOException {
return createPayload("{\"field\":" + value + "}");
}
@SuppressWarnings("unchecked")
private Map<String, Object> createPayload(String json) throws IOException {
return new ObjectMapper().readValue(json, Map.class);
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}