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') {
|
project(':spring-restdocs') {
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
hamcrestVersion = '1.3'
|
||||||
jacksonVersion = '2.3.4'
|
jacksonVersion = '2.3.4'
|
||||||
jacocoVersion = '0.7.2.201409121644'
|
jacocoVersion = '0.7.2.201409121644'
|
||||||
junitVersion = '4.11'
|
junitVersion = '4.11'
|
||||||
@@ -67,6 +68,8 @@ project(':spring-restdocs') {
|
|||||||
testCompile "org.springframework:spring-webmvc:$springVersion"
|
testCompile "org.springframework:spring-webmvc:$springVersion"
|
||||||
testCompile "org.springframework.hateoas:spring-hateoas:$springHateoasVersion"
|
testCompile "org.springframework.hateoas:spring-hateoas:$springHateoasVersion"
|
||||||
testCompile "org.mockito:mockito-core:$mockitoVersion"
|
testCompile "org.mockito:mockito-core:$mockitoVersion"
|
||||||
|
testCompile "org.hamcrest:hamcrest-core:$hamcrestVersion"
|
||||||
|
testCompile "org.hamcrest:hamcrest-library:$hamcrestVersion"
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
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.IOException;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -46,8 +45,6 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
|
|||||||
|
|
||||||
private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver();
|
private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver();
|
||||||
|
|
||||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
|
||||||
|
|
||||||
private final FieldValidator fieldValidator = new FieldValidator();
|
private final FieldValidator fieldValidator = new FieldValidator();
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
@@ -73,18 +70,6 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
|
|||||||
|
|
||||||
final Map<String, Object> payload = extractPayload(result);
|
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() {
|
writer.table(new TableAction() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import org.springframework.util.StringUtils;
|
|||||||
*/
|
*/
|
||||||
public enum FieldType {
|
public enum FieldType {
|
||||||
|
|
||||||
ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING;
|
ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING, VARIES;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
|||||||
@@ -26,10 +26,25 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
class FieldTypeResolver {
|
class FieldTypeResolver {
|
||||||
|
|
||||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
private final FieldProcessor fieldProcessor = new FieldProcessor();
|
||||||
|
|
||||||
FieldType resolveFieldType(String path, Map<String, Object> payload) {
|
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) {
|
private FieldType determineFieldType(Object fieldValue) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ package org.springframework.restdocs.payload;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ import com.fasterxml.jackson.databind.SerializationFeature;
|
|||||||
*/
|
*/
|
||||||
class FieldValidator {
|
class FieldValidator {
|
||||||
|
|
||||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
private final FieldProcessor fieldProcessor = new FieldProcessor();
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper()
|
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||||
.enable(SerializationFeature.INDENT_OUTPUT);
|
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||||
@@ -69,7 +68,8 @@ class FieldValidator {
|
|||||||
|
|
||||||
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
||||||
if (!fieldDescriptor.isOptional()
|
if (!fieldDescriptor.isOptional()
|
||||||
&& !this.fieldExtractor.hasField(fieldDescriptor.getPath(), payload)) {
|
&& !this.fieldProcessor.hasField(
|
||||||
|
FieldPath.compile(fieldDescriptor.getPath()), payload)) {
|
||||||
missingFields.add(fieldDescriptor.getPath());
|
missingFields.add(fieldDescriptor.getPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,33 +80,12 @@ class FieldValidator {
|
|||||||
private Map<String, Object> findUndocumentedFields(Map<String, Object> payload,
|
private Map<String, Object> findUndocumentedFields(Map<String, Object> payload,
|
||||||
List<FieldDescriptor> fieldDescriptors) {
|
List<FieldDescriptor> fieldDescriptors) {
|
||||||
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
||||||
String path = fieldDescriptor.getPath();
|
FieldPath path = FieldPath.compile(fieldDescriptor.getPath());
|
||||||
List<String> segments = path.indexOf('.') > -1 ? Arrays.asList(path
|
this.fieldProcessor.remove(path, payload);
|
||||||
.split("\\.")) : Arrays.asList(path);
|
|
||||||
removeField(segments, 0, payload);
|
|
||||||
}
|
}
|
||||||
return 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")
|
@SuppressWarnings("serial")
|
||||||
static class FieldValidationException extends RuntimeException {
|
static class FieldValidationException extends RuntimeException {
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,56 @@ public abstract class PayloadDocumentation {
|
|||||||
/**
|
/**
|
||||||
* Creates a {@code FieldDescriptor} that describes a field with the given
|
* Creates a {@code FieldDescriptor} that describes a field with the given
|
||||||
* {@code path}.
|
* {@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
|
* @param path The path of the field
|
||||||
* @return a {@code FieldDescriptor} ready for further configuration
|
* @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));
|
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
|
@Test
|
||||||
public void nonExistentFieldProducesIllegalArgumentException() throws IOException {
|
public void nonExistentFieldProducesIllegalArgumentException() throws IOException {
|
||||||
this.thrownException.expect(IllegalArgumentException.class);
|
this.thrownException.expect(IllegalArgumentException.class);
|
||||||
|
|||||||
@@ -38,26 +38,28 @@ public class FieldValidatorTests {
|
|||||||
@Rule
|
@Rule
|
||||||
public ExpectedException thrownException = ExpectedException.none();
|
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
|
@Test
|
||||||
public void noMissingFieldsAllFieldsDocumented() throws IOException {
|
public void noMissingFieldsAllFieldsDocumented() throws IOException {
|
||||||
this.fieldValidator.validate(this.payload, Arrays.asList(
|
this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor(
|
||||||
new FieldDescriptor("a"), new FieldDescriptor("a.b"),
|
"a.b"), new FieldDescriptor("a.c"), new FieldDescriptor("a.d[].e"),
|
||||||
new FieldDescriptor("a.c")));
|
new FieldDescriptor("a.d"), new FieldDescriptor("a")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void optionalFieldsAreNotReportedMissing() throws IOException {
|
public void optionalFieldsAreNotReportedMissing() throws IOException {
|
||||||
this.fieldValidator.validate(this.payload, Arrays.asList(
|
this.fieldValidator.validate(this.payload, Arrays.asList(
|
||||||
new FieldDescriptor("a"), new FieldDescriptor("a.b"),
|
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
|
@Test
|
||||||
public void parentIsDocumentedWhenAllChildrenAreDocumented() throws IOException {
|
public void parentIsDocumentedWhenAllChildrenAreDocumented() throws IOException {
|
||||||
this.fieldValidator.validate(this.payload,
|
this.fieldValidator.validate(this.payload, Arrays.asList(new FieldDescriptor(
|
||||||
Arrays.asList(new FieldDescriptor("a.b"), new FieldDescriptor("a.c")));
|
"a.b"), new FieldDescriptor("a.c"), new FieldDescriptor("a.d[].e")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -83,6 +85,6 @@ public class FieldValidatorTests {
|
|||||||
.expectMessage(equalTo(String
|
.expectMessage(equalTo(String
|
||||||
.format("Portions of the payload were not documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}")));
|
.format("Portions of the payload were not documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}")));
|
||||||
this.fieldValidator.validate(this.payload,
|
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