Add support for documenting fields in payloads that contain arrays
Previously the path used to document a field had to be dot-separated,
with each segment of the path being used as a key in a map. This made
it impossible to document fields in a payload within an array.
This commit adds support for using [] in a field’s path to represent an
array. For example, the fields in following JSON payload:
{
"id": 67,
“date”: "2015-01-20",
"assets": [
{
"id":356,
"name": "sample"
}
]
}
Can now be documented using the following paths:
- id
- date
- assets[].id
- assets[].name
Closes gh-60
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
project(':spring-restdocs') {
|
||||
|
||||
ext {
|
||||
hamcrestVersion = '1.3'
|
||||
jacksonVersion = '2.3.4'
|
||||
jacocoVersion = '0.7.2.201409121644'
|
||||
junitVersion = '4.11'
|
||||
@@ -67,6 +68,8 @@ project(':spring-restdocs') {
|
||||
testCompile "org.springframework:spring-webmvc:$springVersion"
|
||||
testCompile "org.springframework.hateoas:spring-hateoas:$springHateoasVersion"
|
||||
testCompile "org.mockito:mockito-core:$mockitoVersion"
|
||||
testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion"
|
||||
testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion"
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package org.springframework.restdocs.payload;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link FieldExtractor} extracts a field from a payload
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*
|
||||
*/
|
||||
class FieldExtractor {
|
||||
|
||||
boolean hasField(String path, Map<String, Object> payload) {
|
||||
String[] segments = path.indexOf('.') > -1 ? path.split("\\.")
|
||||
: new String[] { path };
|
||||
|
||||
Object current = payload;
|
||||
|
||||
for (String segment : segments) {
|
||||
if (current instanceof Map && ((Map<?, ?>) current).containsKey(segment)) {
|
||||
current = ((Map<?, ?>) current).get(segment);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Object extractField(String path, Map<String, Object> payload) {
|
||||
String[] segments = path.indexOf('.') > -1 ? path.split("\\.")
|
||||
: new String[] { path };
|
||||
|
||||
Object current = payload;
|
||||
|
||||
for (String segment : segments) {
|
||||
if (current instanceof Map && ((Map<?, ?>) current).containsKey(segment)) {
|
||||
current = ((Map<?, ?>) current).get(segment);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException(
|
||||
"The payload does not contain a field with the path '" + path
|
||||
+ "'");
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A path that identifies a field in a payload
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*
|
||||
*/
|
||||
class FieldPath {
|
||||
|
||||
private static final Pattern ARRAY_INDEX_PATTERN = Pattern
|
||||
.compile("\\[([0-9]+|\\*){0,1}\\]");
|
||||
|
||||
private final String rawPath;
|
||||
|
||||
private final List<String> segments;
|
||||
|
||||
private final boolean precise;
|
||||
|
||||
private FieldPath(String rawPath, List<String> segments, boolean precise) {
|
||||
this.rawPath = rawPath;
|
||||
this.segments = segments;
|
||||
this.precise = precise;
|
||||
}
|
||||
|
||||
boolean isPrecise() {
|
||||
return this.precise;
|
||||
}
|
||||
|
||||
List<String> getSegments() {
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.rawPath;
|
||||
}
|
||||
|
||||
static FieldPath compile(String path) {
|
||||
List<String> segments = extractSegments(path);
|
||||
return new FieldPath(path, segments, matchesSingleValue(segments));
|
||||
}
|
||||
|
||||
static boolean isArraySegment(String segment) {
|
||||
return ARRAY_INDEX_PATTERN.matcher(segment).matches();
|
||||
}
|
||||
|
||||
static boolean matchesSingleValue(List<String> segments) {
|
||||
for (String segment : segments) {
|
||||
if (isArraySegment(segment)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static List<String> extractSegments(String path) {
|
||||
Matcher matcher = ARRAY_INDEX_PATTERN.matcher(path);
|
||||
String processedPath;
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
matcher.appendReplacement(buffer, ".[$1]");
|
||||
}
|
||||
matcher.appendTail(buffer);
|
||||
|
||||
if (buffer.length() > 0) {
|
||||
processedPath = buffer.toString();
|
||||
}
|
||||
else {
|
||||
processedPath = path;
|
||||
}
|
||||
|
||||
return Arrays.asList(processedPath.indexOf('.') > -1 ? processedPath.split("\\.")
|
||||
: new String[] { processedPath });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* A {@code FieldProcessor} processes a payload's fields, allowing them to be extracted
|
||||
* and removed
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*
|
||||
*/
|
||||
class FieldProcessor {
|
||||
|
||||
boolean hasField(FieldPath fieldPath, Map<String, Object> payload) {
|
||||
final AtomicReference<Boolean> hasField = new AtomicReference<Boolean>(false);
|
||||
traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public boolean foundMatch(Match match) {
|
||||
hasField.set(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchNotFound() {
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
return hasField.get();
|
||||
}
|
||||
|
||||
Object extract(final FieldPath path, Map<String, Object> payload) {
|
||||
final List<Object> matches = new ArrayList<Object>();
|
||||
traverse(new ProcessingContext(payload, path), new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public boolean foundMatch(Match match) {
|
||||
matches.add(match.getValue());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchNotFound() {
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
if (matches.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"The payload does not contain a field with the path '" + path + "'");
|
||||
}
|
||||
if (path.isPrecise()) {
|
||||
return matches.get(0);
|
||||
}
|
||||
else {
|
||||
return matches;
|
||||
}
|
||||
}
|
||||
|
||||
void remove(final FieldPath path, final Map<String, Object> payload) {
|
||||
traverse(new ProcessingContext(payload, path), new MatchCallback() {
|
||||
|
||||
@Override
|
||||
public boolean foundMatch(Match match) {
|
||||
match.remove();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matchNotFound() {
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private boolean traverse(ProcessingContext context, MatchCallback matchCallback) {
|
||||
final String segment = context.getSegment();
|
||||
if (FieldPath.isArraySegment(segment)) {
|
||||
if (context.getPayload() instanceof List) {
|
||||
return handleListPayload(context, matchCallback);
|
||||
}
|
||||
}
|
||||
else if (context.getPayload() instanceof Map
|
||||
&& ((Map<?, ?>) context.getPayload()).containsKey(segment)) {
|
||||
return handleMapPayload(context, matchCallback);
|
||||
}
|
||||
|
||||
return matchCallback.matchNotFound();
|
||||
}
|
||||
|
||||
private boolean handleListPayload(ProcessingContext context,
|
||||
MatchCallback matchCallback) {
|
||||
List<?> list = context.getPayload();
|
||||
final Iterator<?> items = list.iterator();
|
||||
if (context.isLeaf()) {
|
||||
while (items.hasNext()) {
|
||||
Object item = items.next();
|
||||
if (!matchCallback.foundMatch(new ListMatch(items, list, item, context
|
||||
.getParentMatch()))) {
|
||||
return false;
|
||||
}
|
||||
;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
boolean result = true;
|
||||
while (items.hasNext() && result) {
|
||||
Object item = items.next();
|
||||
result = result
|
||||
&& traverse(context.descend(item, new ListMatch(items, list,
|
||||
item, context.parent)), matchCallback);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleMapPayload(ProcessingContext context,
|
||||
MatchCallback matchCallback) {
|
||||
Map<?, ?> map = context.getPayload();
|
||||
final Object item = map.get(context.getSegment());
|
||||
MapMatch mapMatch = new MapMatch(item, map, context.getSegment(),
|
||||
context.getParentMatch());
|
||||
if (context.isLeaf()) {
|
||||
return matchCallback.foundMatch(mapMatch);
|
||||
}
|
||||
else {
|
||||
return traverse(context.descend(item, mapMatch), matchCallback);
|
||||
}
|
||||
}
|
||||
|
||||
private final class MapMatch implements Match {
|
||||
|
||||
private final Object item;
|
||||
|
||||
private final Map<?, ?> map;
|
||||
|
||||
private final String segment;
|
||||
|
||||
private final Match parent;
|
||||
|
||||
private MapMatch(Object item, Map<?, ?> map, String segment, Match parent) {
|
||||
this.item = item;
|
||||
this.map = map;
|
||||
this.segment = segment;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return this.item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
this.map.remove(this.segment);
|
||||
if (this.map.isEmpty() && this.parent != null) {
|
||||
this.parent.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private final class ListMatch implements Match {
|
||||
|
||||
private final Iterator<?> items;
|
||||
|
||||
private final List<?> list;
|
||||
|
||||
private final Object item;
|
||||
|
||||
private final Match parent;
|
||||
|
||||
private ListMatch(Iterator<?> items, List<?> list, Object item, Match parent) {
|
||||
this.items = items;
|
||||
this.list = list;
|
||||
this.item = item;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return this.item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
this.items.remove();
|
||||
if (this.list.isEmpty() && this.parent != null) {
|
||||
this.parent.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private interface MatchCallback {
|
||||
|
||||
boolean foundMatch(Match match);
|
||||
|
||||
boolean matchNotFound();
|
||||
}
|
||||
|
||||
private interface Match {
|
||||
|
||||
Object getValue();
|
||||
|
||||
void remove();
|
||||
}
|
||||
|
||||
private static class ProcessingContext {
|
||||
|
||||
private final Object payload;
|
||||
|
||||
private final List<String> segments;
|
||||
|
||||
private final Match parent;
|
||||
|
||||
private final FieldPath path;
|
||||
|
||||
private ProcessingContext(Object payload, FieldPath path) {
|
||||
this(payload, path, null, null);
|
||||
}
|
||||
|
||||
private ProcessingContext(Object payload, FieldPath path, List<String> segments,
|
||||
Match parent) {
|
||||
this.payload = payload;
|
||||
this.path = path;
|
||||
this.segments = segments == null ? path.getSegments() : segments;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
private String getSegment() {
|
||||
return this.segments.get(0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getPayload() {
|
||||
return (T) this.payload;
|
||||
}
|
||||
|
||||
private boolean isLeaf() {
|
||||
return this.segments.size() == 1;
|
||||
}
|
||||
|
||||
private Match getParentMatch() {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
private ProcessingContext descend(Object payload, Match match) {
|
||||
return new ProcessingContext(payload, this.path, this.segments.subList(1,
|
||||
this.segments.size()), match);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,6 @@ package org.springframework.restdocs.payload;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -46,8 +45,6 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
|
||||
|
||||
private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver();
|
||||
|
||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
||||
|
||||
private final FieldValidator fieldValidator = new FieldValidator();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
@@ -73,18 +70,6 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
|
||||
|
||||
final Map<String, Object> payload = extractPayload(result);
|
||||
|
||||
List<String> missingFields = new ArrayList<String>();
|
||||
|
||||
for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) {
|
||||
if (!fieldDescriptor.isOptional()) {
|
||||
Object field = this.fieldExtractor.extractField(
|
||||
fieldDescriptor.getPath(), payload);
|
||||
if (field == null) {
|
||||
missingFields.add(fieldDescriptor.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.table(new TableAction() {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,7 +27,7 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
public enum FieldType {
|
||||
|
||||
ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING;
|
||||
ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING, VARIES;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -26,10 +26,25 @@ import java.util.Map;
|
||||
*/
|
||||
class FieldTypeResolver {
|
||||
|
||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
||||
private final FieldProcessor fieldProcessor = new FieldProcessor();
|
||||
|
||||
FieldType resolveFieldType(String path, Map<String, Object> payload) {
|
||||
return determineFieldType(this.fieldExtractor.extractField(path, payload));
|
||||
FieldPath fieldPath = FieldPath.compile(path);
|
||||
Object field = this.fieldProcessor.extract(fieldPath, payload);
|
||||
if (field instanceof Collection && !fieldPath.isPrecise()) {
|
||||
FieldType commonType = null;
|
||||
for (Object item : (Collection<?>) field) {
|
||||
FieldType fieldType = determineFieldType(item);
|
||||
if (commonType == null) {
|
||||
commonType = fieldType;
|
||||
}
|
||||
else if (fieldType != commonType) {
|
||||
return FieldType.VARIES;
|
||||
}
|
||||
}
|
||||
return commonType;
|
||||
}
|
||||
return determineFieldType(this.fieldProcessor.extract(fieldPath, payload));
|
||||
}
|
||||
|
||||
private FieldType determineFieldType(Object fieldValue) {
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.springframework.restdocs.payload;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -34,7 +33,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
*/
|
||||
class FieldValidator {
|
||||
|
||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
||||
private final FieldProcessor fieldProcessor = new FieldProcessor();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
@@ -69,7 +68,8 @@ class FieldValidator {
|
||||
|
||||
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
||||
if (!fieldDescriptor.isOptional()
|
||||
&& !this.fieldExtractor.hasField(fieldDescriptor.getPath(), payload)) {
|
||||
&& !this.fieldProcessor.hasField(
|
||||
FieldPath.compile(fieldDescriptor.getPath()), payload)) {
|
||||
missingFields.add(fieldDescriptor.getPath());
|
||||
}
|
||||
}
|
||||
@@ -80,33 +80,12 @@ class FieldValidator {
|
||||
private Map<String, Object> findUndocumentedFields(Map<String, Object> payload,
|
||||
List<FieldDescriptor> fieldDescriptors) {
|
||||
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
||||
String path = fieldDescriptor.getPath();
|
||||
List<String> segments = path.indexOf('.') > -1 ? Arrays.asList(path
|
||||
.split("\\.")) : Arrays.asList(path);
|
||||
removeField(segments, 0, payload);
|
||||
FieldPath path = FieldPath.compile(fieldDescriptor.getPath());
|
||||
this.fieldProcessor.remove(path, payload);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private void removeField(List<String> segments, int depth,
|
||||
Map<String, Object> payloadPortion) {
|
||||
String key = segments.get(depth);
|
||||
if (depth == segments.size() - 1) {
|
||||
payloadPortion.remove(key);
|
||||
}
|
||||
else {
|
||||
Object candidate = payloadPortion.get(key);
|
||||
if (candidate instanceof Map) {
|
||||
Map map = (Map<?, ?>) candidate;
|
||||
removeField(segments, depth + 1, map);
|
||||
if (map.isEmpty()) {
|
||||
payloadPortion.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class FieldValidationException extends RuntimeException {
|
||||
|
||||
|
||||
@@ -35,6 +35,56 @@ public abstract class PayloadDocumentation {
|
||||
/**
|
||||
* Creates a {@code FieldDescriptor} that describes a field with the given
|
||||
* {@code path}.
|
||||
* <p>
|
||||
* The {@code path} uses '.' to descend into a child object and ' {@code []}' to
|
||||
* descend into an array. For example, with this JSON payload:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "a":{
|
||||
* "b":[
|
||||
* {
|
||||
* "c":"one"
|
||||
* },
|
||||
* {
|
||||
* "c":"two"
|
||||
* },
|
||||
* {
|
||||
* "d":"three"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* The following paths are all present:
|
||||
*
|
||||
* <table summary="Paths and their values">
|
||||
* <tr>
|
||||
* <th>Path</th>
|
||||
* <th>Value</th>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@code a}</td>
|
||||
* <td>An object containing "b"</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@code a.b}</td>
|
||||
* <td>An array containing three objects</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@code a.b[]}</td>
|
||||
* <td>An array containing three objects</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@code a.b[].c}</td>
|
||||
* <td>An array containing the strings "one" and "two"</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>{@code a.b[].d}</td>
|
||||
* <td>The string "three"</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
*
|
||||
* @param path The path of the field
|
||||
* @return a {@code FieldDescriptor} ready for further configuration
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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 static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link FieldPath}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class FieldPathTests {
|
||||
|
||||
@Test
|
||||
public void singleFieldIsPrecise() {
|
||||
assertTrue(FieldPath.compile("a").isPrecise());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleNestedFieldIsPrecise() {
|
||||
assertTrue(FieldPath.compile("a.b").isPrecise());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arrayIsNotPrecise() {
|
||||
assertFalse(FieldPath.compile("a[]").isPrecise());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedArrayIsNotPrecise() {
|
||||
assertFalse(FieldPath.compile("a.b[]").isPrecise());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void arrayOfArraysIsNotPrecise() {
|
||||
assertFalse(FieldPath.compile("a[][]").isPrecise());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fieldBeneathAnArrayIsNotPrecise() {
|
||||
assertFalse(FieldPath.compile("a[].b").isPrecise());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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 static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Tests for {@link FieldProcessor}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class FieldProcessorTests {
|
||||
|
||||
private final FieldProcessor fieldProcessor = new FieldProcessor();
|
||||
|
||||
@Test
|
||||
public void extractTopLevelMapEntry() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("a", "alpha");
|
||||
assertThat(this.fieldProcessor.extract(FieldPath.compile("a"), payload),
|
||||
equalTo((Object) "alpha"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractNestedMapEntry() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
Map<String, Object> alpha = new HashMap<>();
|
||||
payload.put("a", alpha);
|
||||
alpha.put("b", "bravo");
|
||||
assertThat(this.fieldProcessor.extract(FieldPath.compile("a.b"), payload),
|
||||
equalTo((Object) "bravo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractArray() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
Map<String, Object> bravo = new HashMap<>();
|
||||
bravo.put("b", "bravo");
|
||||
List<Map<String, Object>> alpha = Arrays.asList(bravo, bravo);
|
||||
payload.put("a", alpha);
|
||||
assertThat(this.fieldProcessor.extract(FieldPath.compile("a"), payload),
|
||||
equalTo((Object) alpha));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractArrayContents() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
Map<String, Object> bravo = new HashMap<>();
|
||||
bravo.put("b", "bravo");
|
||||
List<Map<String, Object>> alpha = Arrays.asList(bravo, bravo);
|
||||
payload.put("a", alpha);
|
||||
assertThat(this.fieldProcessor.extract(FieldPath.compile("a[]"), payload),
|
||||
equalTo((Object) alpha));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractFromItemsInArray() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
Map<String, Object> entry = new HashMap<>();
|
||||
entry.put("b", "bravo");
|
||||
List<Map<String, Object>> alpha = Arrays.asList(entry, entry);
|
||||
payload.put("a", alpha);
|
||||
assertThat(this.fieldProcessor.extract(FieldPath.compile("a[].b"), payload),
|
||||
equalTo((Object) Arrays.asList("bravo", "bravo")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractNestedArray() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
Map<String, String> entry1 = createEntry("id:1");
|
||||
Map<String, String> entry2 = createEntry("id:2");
|
||||
Map<String, String> entry3 = createEntry("id:3");
|
||||
List<List<Map<String, String>>> alpha = Arrays.asList(
|
||||
Arrays.asList(entry1, entry2), Arrays.asList(entry3));
|
||||
payload.put("a", alpha);
|
||||
assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][]"), payload),
|
||||
equalTo((Object) Arrays.asList(entry1, entry2, entry3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractFromItemsInNestedArray() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
Map<String, String> entry1 = createEntry("id:1");
|
||||
Map<String, String> entry2 = createEntry("id:2");
|
||||
Map<String, String> entry3 = createEntry("id:3");
|
||||
List<List<Map<String, String>>> alpha = Arrays.asList(
|
||||
Arrays.asList(entry1, entry2), Arrays.asList(entry3));
|
||||
payload.put("a", alpha);
|
||||
assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][].id"), payload),
|
||||
equalTo((Object) Arrays.asList("1", "2", "3")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void extractArraysFromItemsInNestedArray() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
Map<String, Object> entry1 = createEntry("ids", Arrays.asList(1, 2));
|
||||
Map<String, Object> entry2 = createEntry("ids", Arrays.asList(3));
|
||||
Map<String, Object> entry3 = createEntry("ids", Arrays.asList(4));
|
||||
List<List<Map<String, Object>>> alpha = Arrays.asList(
|
||||
Arrays.asList(entry1, entry2), Arrays.asList(entry3));
|
||||
payload.put("a", alpha);
|
||||
assertThat(this.fieldProcessor.extract(FieldPath.compile("a[][].ids"), payload),
|
||||
equalTo((Object) Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3),
|
||||
Arrays.asList(4))));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nonExistentTopLevelField() {
|
||||
this.fieldProcessor
|
||||
.extract(FieldPath.compile("a"), new HashMap<String, Object>());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nonExistentNestedField() {
|
||||
HashMap<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("a", new HashMap<String, Object>());
|
||||
this.fieldProcessor.extract(FieldPath.compile("a.b"), payload);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nonExistentNestedFieldWhenParentIsNotAMap() {
|
||||
HashMap<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("a", 5);
|
||||
this.fieldProcessor.extract(FieldPath.compile("a.b"), payload);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nonExistentFieldWhenParentIsAnArray() {
|
||||
HashMap<String, Object> payload = new HashMap<String, Object>();
|
||||
HashMap<String, Object> alpha = new HashMap<String, Object>();
|
||||
alpha.put("b", Arrays.asList(new HashMap<String, Object>()));
|
||||
payload.put("a", alpha);
|
||||
this.fieldProcessor.extract(FieldPath.compile("a.b.c"), payload);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nonExistentArrayField() {
|
||||
HashMap<String, Object> payload = new HashMap<String, Object>();
|
||||
this.fieldProcessor.extract(FieldPath.compile("a[]"), payload);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nonExistentArrayFieldAsTypeDoesNotMatch() {
|
||||
HashMap<String, Object> payload = new HashMap<String, Object>();
|
||||
payload.put("a", 5);
|
||||
this.fieldProcessor.extract(FieldPath.compile("a[]"), payload);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nonExistentFieldBeneathAnArray() {
|
||||
HashMap<String, Object> payload = new HashMap<String, Object>();
|
||||
HashMap<String, Object> alpha = new HashMap<String, Object>();
|
||||
alpha.put("b", Arrays.asList(new HashMap<String, Object>()));
|
||||
payload.put("a", alpha);
|
||||
this.fieldProcessor.extract(FieldPath.compile("a.b[].id"), payload);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeTopLevelMapEntry() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("a", "alpha");
|
||||
this.fieldProcessor.remove(FieldPath.compile("a"), payload);
|
||||
assertThat(payload.size(), equalTo(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeNestedMapEntry() {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
Map<String, Object> alpha = new HashMap<>();
|
||||
payload.put("a", alpha);
|
||||
alpha.put("b", "bravo");
|
||||
this.fieldProcessor.remove(FieldPath.compile("a.b"), payload);
|
||||
assertThat(payload.size(), equalTo(0));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void removeItemsInArray() throws IOException {
|
||||
Map<String, Object> payload = new ObjectMapper().readValue(
|
||||
"{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class);
|
||||
this.fieldProcessor.remove(FieldPath.compile("a[].b"), payload);
|
||||
assertThat(payload.size(), equalTo(0));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void removeItemsInNestedArray() throws IOException {
|
||||
Map<String, Object> payload = new ObjectMapper().readValue(
|
||||
"{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class);
|
||||
this.fieldProcessor.remove(FieldPath.compile("a[][].id"), payload);
|
||||
assertThat(payload.size(), equalTo(0));
|
||||
}
|
||||
|
||||
private Map<String, String> createEntry(String... pairs) {
|
||||
Map<String, String> entry = new HashMap<>();
|
||||
for (String pair : pairs) {
|
||||
String[] components = pair.split(":");
|
||||
entry.put(components[0], components[1]);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private Map<String, Object> createEntry(String key, Object value) {
|
||||
Map<String, Object> entry = new HashMap<>();
|
||||
entry.put(key, value);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,20 @@ public class FieldTypeResolverTests {
|
||||
createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(FieldType.OBJECT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFieldsWithSameType() throws IOException {
|
||||
assertThat(this.fieldTypeResolver.resolveFieldType("a[].id",
|
||||
createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")),
|
||||
equalTo(FieldType.NUMBER));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFieldsWithDifferentTypes() throws IOException {
|
||||
assertThat(this.fieldTypeResolver.resolveFieldType("a[].id",
|
||||
createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")),
|
||||
equalTo(FieldType.VARIES));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonExistentFieldProducesIllegalArgumentException() throws IOException {
|
||||
this.thrownException.expect(IllegalArgumentException.class);
|
||||
|
||||
@@ -38,26 +38,28 @@ public class FieldValidatorTests {
|
||||
@Rule
|
||||
public ExpectedException thrownException = ExpectedException.none();
|
||||
|
||||
private StringReader payload = new StringReader("{\"a\":{\"b\":{}, \"c\":true}}");
|
||||
private StringReader payload = new StringReader(
|
||||
"{\"a\":{\"b\":{},\"c\":true,\"d\":[{\"e\":1},{\"e\":2}]}}");
|
||||
|
||||
@Test
|
||||
public void noMissingFieldsAllFieldsDocumented() throws IOException {
|
||||
this.fieldValidator.validate(this.payload, Arrays.asList(
|
||||
new FieldDescriptor("a"), new FieldDescriptor("a.b"),
|
||||
new FieldDescriptor("a.c")));
|
||||
this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor(
|
||||
"a.b"), new FieldDescriptor("a.c"), new FieldDescriptor("a.d[].e"),
|
||||
new FieldDescriptor("a.d"), new FieldDescriptor("a")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionalFieldsAreNotReportedMissing() throws IOException {
|
||||
this.fieldValidator.validate(this.payload, Arrays.asList(
|
||||
new FieldDescriptor("a"), new FieldDescriptor("a.b"),
|
||||
new FieldDescriptor("a.c"), new FieldDescriptor("y").optional()));
|
||||
new FieldDescriptor("a.c"), new FieldDescriptor("a.d"),
|
||||
new FieldDescriptor("y").optional()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parentIsDocumentedWhenAllChildrenAreDocumented() throws IOException {
|
||||
this.fieldValidator.validate(this.payload,
|
||||
Arrays.asList(new FieldDescriptor("a.b"), new FieldDescriptor("a.c")));
|
||||
this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor(
|
||||
"a.b"), new FieldDescriptor("a.c"), new FieldDescriptor("a.d[].e")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -83,6 +85,6 @@ public class FieldValidatorTests {
|
||||
.expectMessage(equalTo(String
|
||||
.format("Portions of the payload were not documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}")));
|
||||
this.fieldValidator.validate(this.payload,
|
||||
Arrays.asList(new FieldDescriptor("a.b")));
|
||||
Arrays.asList(new FieldDescriptor("a.b"), new FieldDescriptor("a.d")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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 static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.core.IsEqual.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.collection.IsIterableContainingInAnyOrder;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.restdocs.StubMvcResult;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link PayloadDocumentation}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class PayloadDocumentationTests {
|
||||
|
||||
private final File outputDir = new File("build/payload-documentation-tests");
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
System.setProperty("org.springframework.restdocs.outputDir",
|
||||
this.outputDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
System.clearProperty("org.springframework.restdocs.outputDir");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWithFields() throws IOException {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
|
||||
request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes());
|
||||
documentRequestFields("request-with-fields",
|
||||
fieldWithPath("a.b").description("one"),
|
||||
fieldWithPath("a.c").description("two"),
|
||||
fieldWithPath("a").description("three")).handle(
|
||||
new StubMvcResult(request, null));
|
||||
assertThat(
|
||||
snippet("request-with-fields", "request-fields"),
|
||||
is(asciidoctorTableWith(header("Path", "Type", "Description"),
|
||||
row("a.b", "Number", "one"), row("a.c", "String", "two"),
|
||||
row("a", "Object", "three"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void responseWithFields() throws IOException {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
response.getWriter()
|
||||
.append("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\": [{\"id\":356,\"name\": \"sample\"}]}");
|
||||
documentResponseFields("response-with-fields",
|
||||
fieldWithPath("id").description("one"),
|
||||
fieldWithPath("date").description("two"),
|
||||
fieldWithPath("assets").description("three"),
|
||||
fieldWithPath("assets[]").description("four"),
|
||||
fieldWithPath("assets[].id").description("five"),
|
||||
fieldWithPath("assets[].name").description("six")).handle(
|
||||
new StubMvcResult(new MockHttpServletRequest("GET", "/"), response));
|
||||
assertThat(
|
||||
snippet("response-with-fields", "response-fields"),
|
||||
is(asciidoctorTableWith(header("Path", "Type", "Description"),
|
||||
row("id", "Number", "one"), row("date", "String", "two"),
|
||||
row("assets", "Array", "three"),
|
||||
row("assets[]", "Object", "four"),
|
||||
row("assets[].id", "Number", "five"),
|
||||
row("assets[].name", "String", "six"))));
|
||||
}
|
||||
|
||||
private Matcher<Iterable<? extends String>> asciidoctorTableWith(String[] header,
|
||||
String[]... rows) {
|
||||
Collection<Matcher<? super String>> matchers = new ArrayList<Matcher<? super String>>();
|
||||
for (String headerItem : header) {
|
||||
matchers.add(equalTo(headerItem));
|
||||
}
|
||||
|
||||
for (String[] row : rows) {
|
||||
for (String rowItem : row) {
|
||||
matchers.add(equalTo(rowItem));
|
||||
}
|
||||
}
|
||||
|
||||
matchers.add(equalTo("|==="));
|
||||
matchers.add(equalTo(""));
|
||||
|
||||
return new IsIterableContainingInAnyOrder<String>(matchers);
|
||||
}
|
||||
|
||||
private String[] header(String... columns) {
|
||||
String header = "|"
|
||||
+ StringUtils.collectionToDelimitedString(Arrays.asList(columns), "|");
|
||||
return new String[] { "", "|===", header, "" };
|
||||
}
|
||||
|
||||
private String[] row(String... entries) {
|
||||
List<String> lines = new ArrayList<String>();
|
||||
for (String entry : entries) {
|
||||
lines.add("|" + entry);
|
||||
}
|
||||
lines.add("");
|
||||
return lines.toArray(new String[lines.size()]);
|
||||
}
|
||||
|
||||
private List<String> snippet(String snippetName, String snippetType)
|
||||
throws IOException {
|
||||
File snippetDir = new File(this.outputDir, snippetName);
|
||||
File snippetFile = new File(snippetDir, snippetType + ".adoc");
|
||||
String line = null;
|
||||
List<String> lines = new ArrayList<String>();
|
||||
BufferedReader reader = new BufferedReader(new FileReader(snippetFile));
|
||||
try {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
lines.add(line);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
reader.close();
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user