Add support for documenting fields in XML payloads

Closes gh-46
This commit is contained in:
Andy Wilkinson
2015-08-19 16:24:29 +01:00
parent 76dd0cc579
commit 3fd65791d1
23 changed files with 684 additions and 394 deletions

View File

@@ -81,12 +81,18 @@ payload and the field has not been marked as optional. For payloads with a hiera
structure, documenting a field is sufficient for all of its descendants to also be
treated as having been documented.
[[documenting-your-api-request-response-payloads-field-paths]]
==== Field paths
TIP: By default, Spring REST Docs will assume that the payload you are documenting is
JSON. If you want to document an XML payload the content type of the request or response
must be compatible with `application/xml`.
When documenting request and response payloads, fields are identified using a path. Paths
use `.` to descend into a child object and `[]` to identify an array. For example, with
this JSON payload:
[[documenting-your-api-request-response-payloads-json]]
==== JSON payloads
[[documenting-your-api-request-response-payloads-json-field-paths]]
===== JSON field paths
JSON field paths use `.` to descend into a child object and `[]` to identify an array. For
example, with this JSON payload:
[source,json,indent=0]
----
@@ -131,8 +137,8 @@ The following paths are all present:
[[documenting-your-api-request-response-payloads-field-types]]
==== Field types
[[documenting-your-api-request-response-payloads-json-field-types]]
===== JSON field types
When a field is documented, Spring REST Docs will attempt to determine its type by
examining the payload. Seven different types are supported:
@@ -163,8 +169,9 @@ examining the payload. Seven different types are supported:
| The field occurs multiple times in the payload with a variety of different types
|===
The type can also be set explicitly using the `type(FieldType)` method on
`FieldDescriptor`:
The type can also be set explicitly using the `type(Object)` method on
`FieldDescriptor`. Typically, one of the values enumerated by `JsonFieldType` will be
used:
[source,java,indent=0]
----
@@ -173,6 +180,24 @@ include::{examples-dir}/com/example/Payload.java[tags=explicit-type]
<1> Set the field's type to `string`.
[[documenting-your-api-request-response-payloads-xml]]
==== XML payloads
[[documenting-your-api-request-response-payloads-xml-field-paths]]
===== XML field paths
XML field paths are described using XPath. `/` is used to descend into a child node.
[[documenting-your-api-request-response-payloads-xml-field-types]]
===== XML field types
When documenting an XML payload, you must provide a type for the field using the
`type(Object)` method on `FieldDescriptor`. The result of the supplied type's `toString`
method will be included in the documentation.
[[documenting-your-api-request-parameters]]
=== Request parameters

View File

@@ -27,7 +27,7 @@ import static org.springframework.restdocs.RestDocumentationRequestBuilders.post
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.FieldType;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
public class Payload {
@@ -50,7 +50,7 @@ private MockMvc mockMvc;
// tag::explicit-type[]
.andDo(document("index", responseFields(
fieldWithPath("contact.email")
.type(FieldType.STRING) // <1>
.type(JsonFieldType.STRING) // <1>
.optional()
.description("The user's email address"))));
// end::explicit-type[]

View File

@@ -18,36 +18,28 @@ package org.springframework.restdocs.payload;
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.MediaType;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
/**
* A {@link TemplatedSnippet} that produces a snippet documenting a
* RESTful resource's request or response fields.
* A {@link TemplatedSnippet} that produces a snippet documenting a RESTful resource's
* request or response fields.
*
* @author Andreas Evers
* @author Andy Wilkinson
*/
public abstract class AbstractFieldsSnippet extends
TemplatedSnippet {
private final Map<String, FieldDescriptor> descriptorsByPath = new LinkedHashMap<String, FieldDescriptor>();
private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver();
private final FieldValidator fieldValidator = new FieldValidator();
private final ObjectMapper objectMapper = new ObjectMapper();
public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
private List<FieldDescriptor> fieldDescriptors;
@@ -57,45 +49,73 @@ public abstract class AbstractFieldsSnippet extends
for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath());
Assert.hasText(descriptor.getDescription());
this.descriptorsByPath.put(descriptor.getPath(), descriptor);
}
this.fieldDescriptors = descriptors;
}
@Override
protected Map<String, Object> document(MvcResult result) throws IOException {
this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors);
Object payload = extractPayload(result);
MediaType contentType = getContentType(result);
PayloadHandler payloadHandler;
if (contentType != null
&& MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
payloadHandler = new XmlPayloadHandler(readPayload(result));
}
else {
payloadHandler = new JsonPayloadHandler(readPayload(result));
}
validateFieldDocumentation(payloadHandler);
for (FieldDescriptor descriptor : this.fieldDescriptors) {
if (descriptor.getType() == null) {
descriptor.type(payloadHandler.determineFieldType(descriptor.getPath()));
}
}
Map<String, Object> model = new HashMap<>();
List<Map<String, Object>> fields = new ArrayList<>();
model.put("fields", fields);
for (Entry<String, FieldDescriptor> entry : this.descriptorsByPath.entrySet()) {
FieldDescriptor descriptor = entry.getValue();
if (descriptor.getType() == null) {
descriptor.type(getFieldType(descriptor, payload));
}
for (FieldDescriptor descriptor : this.fieldDescriptors) {
fields.add(descriptor.toModel());
}
return model;
}
private FieldType getFieldType(FieldDescriptor descriptor, Object payload) {
try {
return AbstractFieldsSnippet.this.fieldTypeResolver
.resolveFieldType(descriptor.getPath(), payload);
}
catch (FieldDoesNotExistException ex) {
String message = "Cannot determine the type of the field '"
+ descriptor.getPath() + "' as it is not present in the"
+ " payload. Please provide a type using"
+ " FieldDescriptor.type(FieldType).";
throw new FieldTypeRequiredException(message);
private String readPayload(MvcResult result) throws IOException {
StringWriter writer = new StringWriter();
FileCopyUtils.copy(getPayloadReader(result), writer);
return writer.toString();
}
private void validateFieldDocumentation(PayloadHandler payloadHandler) {
List<FieldDescriptor> missingFields = payloadHandler
.findMissingFields(this.fieldDescriptors);
String undocumentedPayload = payloadHandler
.getUndocumentedPayload(this.fieldDescriptors);
if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) {
String message = "";
if (StringUtils.hasText(undocumentedPayload)) {
message += String.format("The following parts of the payload were"
+ " not documented:%n%s", undocumentedPayload);
}
if (!missingFields.isEmpty()) {
if (message.length() > 0) {
message += String.format("%n");
}
List<String> paths = new ArrayList<String>();
for (FieldDescriptor fieldDescriptor : missingFields) {
paths.add(fieldDescriptor.getPath());
}
message += "Fields with the following paths were not found in the"
+ " payload: " + paths;
}
throw new SnippetException(message);
}
}
private Object extractPayload(MvcResult result) throws IOException {
return this.objectMapper.readValue(getPayloadReader(result), Object.class);
}
protected abstract MediaType getContentType(MvcResult result);
protected abstract Reader getPayloadReader(MvcResult result) throws IOException;

View File

@@ -33,7 +33,7 @@ public class FieldDescriptor extends AbstractDescriptor<FieldDescriptor> {
private final String path;
private FieldType type;
private Object type;
private boolean optional;
@@ -44,13 +44,14 @@ public class FieldDescriptor extends AbstractDescriptor<FieldDescriptor> {
}
/**
* Specifies the type of the field
* Specifies the type of the field. When documenting a JSON payload, the
* {@link JsonFieldType} enumeration will typically be used.
*
* @param type The type of the field
*
* @return {@code this}
* @see JsonFieldType
*/
public FieldDescriptor type(FieldType type) {
public FieldDescriptor type(Object type) {
this.type = type;
return this;
}
@@ -80,7 +81,7 @@ public class FieldDescriptor extends AbstractDescriptor<FieldDescriptor> {
return this.path;
}
FieldType getType() {
Object getType() {
return this.type;
}

View File

@@ -31,7 +31,7 @@ public class FieldDoesNotExistException extends RuntimeException {
*
* @param fieldPath the path of the field that does not exist
*/
public FieldDoesNotExistException(FieldPath fieldPath) {
public FieldDoesNotExistException(JsonFieldPath fieldPath) {
super("The payload does not contain a field with the path '" + fieldPath + "'");
}
}

View File

@@ -1,98 +0,0 @@
/*
* 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.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.restdocs.snippet.SnippetException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* {@code FieldValidator} is used to validate a payload's fields against the user-provided
* {@link FieldDescriptor}s.
*
* @author Andy Wilkinson
*/
class FieldValidator {
private final FieldProcessor fieldProcessor = new FieldProcessor();
private final ObjectMapper objectMapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
void validate(Reader payloadReader, List<FieldDescriptor> fieldDescriptors)
throws IOException {
Object payload = this.objectMapper.readValue(payloadReader, Object.class);
List<String> missingFields = findMissingFields(payload, fieldDescriptors);
Object undocumentedPayload = findUndocumentedFields(payload, fieldDescriptors);
if (!missingFields.isEmpty() || !isEmpty(undocumentedPayload)) {
String message = "";
if (!isEmpty(undocumentedPayload)) {
message += String.format(
"The following parts of the payload were not documented:%n%s",
this.objectMapper.writeValueAsString(undocumentedPayload));
}
if (!missingFields.isEmpty()) {
if (message.length() > 0) {
message += String.format("%n");
}
message += "Fields with the following paths were not found in the payload: "
+ missingFields;
}
throw new SnippetException(message);
}
}
private boolean isEmpty(Object object) {
if (object instanceof Map) {
return ((Map<?, ?>) object).isEmpty();
}
return ((List<?>) object).isEmpty();
}
private List<String> findMissingFields(Object payload,
List<FieldDescriptor> fieldDescriptors) {
List<String> missingFields = new ArrayList<String>();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
if (!fieldDescriptor.isOptional()
&& !this.fieldProcessor.hasField(
FieldPath.compile(fieldDescriptor.getPath()), payload)) {
missingFields.add(fieldDescriptor.getPath());
}
}
return missingFields;
}
private Object findUndocumentedFields(Object payload,
List<FieldDescriptor> fieldDescriptors) {
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
FieldPath path = FieldPath.compile(fieldDescriptor.getPath());
this.fieldProcessor.remove(path, payload);
}
return payload;
}
}

View File

@@ -22,12 +22,12 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A path that identifies a field in a payload
* A path that identifies a field in a JSON payload
*
* @author Andy Wilkinson
*
*/
final class FieldPath {
final class JsonFieldPath {
private static final Pattern ARRAY_INDEX_PATTERN = Pattern
.compile("\\[([0-9]+|\\*){0,1}\\]");
@@ -38,7 +38,7 @@ final class FieldPath {
private final boolean precise;
private FieldPath(String rawPath, List<String> segments, boolean precise) {
private JsonFieldPath(String rawPath, List<String> segments, boolean precise) {
this.rawPath = rawPath;
this.segments = segments;
this.precise = precise;
@@ -57,9 +57,9 @@ final class FieldPath {
return this.rawPath;
}
static FieldPath compile(String path) {
static JsonFieldPath compile(String path) {
List<String> segments = extractSegments(path);
return new FieldPath(path, segments, matchesSingleValue(segments));
return new JsonFieldPath(path, segments, matchesSingleValue(segments));
}
static boolean isArraySegment(String segment) {

View File

@@ -23,15 +23,15 @@ 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
* A {@code JsonFieldProcessor} processes a payload's fields, allowing them to be
* extracted and removed
*
* @author Andy Wilkinson
*
*/
final class FieldProcessor {
final class JsonFieldProcessor {
boolean hasField(FieldPath fieldPath, Object payload) {
boolean hasField(JsonFieldPath fieldPath, Object payload) {
final AtomicReference<Boolean> hasField = new AtomicReference<Boolean>(false);
traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() {
@@ -44,7 +44,7 @@ final class FieldProcessor {
return hasField.get();
}
Object extract(FieldPath path, Object payload) {
Object extract(JsonFieldPath path, Object payload) {
final List<Object> matches = new ArrayList<Object>();
traverse(new ProcessingContext(payload, path), new MatchCallback() {
@@ -65,7 +65,7 @@ final class FieldProcessor {
}
}
void remove(final FieldPath path, Object payload) {
void remove(final JsonFieldPath path, Object payload) {
traverse(new ProcessingContext(payload, path), new MatchCallback() {
@Override
@@ -78,7 +78,7 @@ final class FieldProcessor {
private void traverse(ProcessingContext context, MatchCallback matchCallback) {
final String segment = context.getSegment();
if (FieldPath.isArraySegment(segment)) {
if (JsonFieldPath.isArraySegment(segment)) {
if (context.getPayload() instanceof List) {
handleListPayload(context, matchCallback);
}
@@ -206,13 +206,13 @@ final class FieldProcessor {
private final Match parent;
private final FieldPath path;
private final JsonFieldPath path;
private ProcessingContext(Object payload, FieldPath path) {
private ProcessingContext(Object payload, JsonFieldPath path) {
this(payload, path, null, null);
}
private ProcessingContext(Object payload, FieldPath path, List<String> segments,
private ProcessingContext(Object payload, JsonFieldPath path, List<String> segments,
Match parent) {
this.payload = payload;
this.path = path;

View File

@@ -25,7 +25,7 @@ import org.springframework.util.StringUtils;
*
* @author Andy Wilkinson
*/
public enum FieldType {
public enum JsonFieldType {
ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING, VARIES;

View File

@@ -20,26 +20,26 @@ import java.util.Collection;
import java.util.Map;
/**
* Resolves the type of a field in a request or response payload
* Resolves the type of a field in a JSON request or response payload
*
* @author Andy Wilkinson
*/
class FieldTypeResolver {
class JsonFieldTypeResolver {
private final FieldProcessor fieldProcessor = new FieldProcessor();
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
FieldType resolveFieldType(String path, Object payload) {
FieldPath fieldPath = FieldPath.compile(path);
JsonFieldType resolveFieldType(String path, Object payload) {
JsonFieldPath fieldPath = JsonFieldPath.compile(path);
Object field = this.fieldProcessor.extract(fieldPath, payload);
if (field instanceof Collection && !fieldPath.isPrecise()) {
FieldType commonType = null;
JsonFieldType commonType = null;
for (Object item : (Collection<?>) field) {
FieldType fieldType = determineFieldType(item);
JsonFieldType fieldType = determineFieldType(item);
if (commonType == null) {
commonType = fieldType;
}
else if (fieldType != commonType) {
return FieldType.VARIES;
return JsonFieldType.VARIES;
}
}
return commonType;
@@ -47,22 +47,22 @@ class FieldTypeResolver {
return determineFieldType(this.fieldProcessor.extract(fieldPath, payload));
}
private FieldType determineFieldType(Object fieldValue) {
private JsonFieldType determineFieldType(Object fieldValue) {
if (fieldValue == null) {
return FieldType.NULL;
return JsonFieldType.NULL;
}
if (fieldValue instanceof String) {
return FieldType.STRING;
return JsonFieldType.STRING;
}
if (fieldValue instanceof Map) {
return FieldType.OBJECT;
return JsonFieldType.OBJECT;
}
if (fieldValue instanceof Collection) {
return FieldType.ARRAY;
return JsonFieldType.ARRAY;
}
if (fieldValue instanceof Boolean) {
return FieldType.BOOLEAN;
return JsonFieldType.BOOLEAN;
}
return FieldType.NUMBER;
return JsonFieldType.NUMBER;
}
}

View File

@@ -0,0 +1,92 @@
package org.springframework.restdocs.payload;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* A {@link PayloadHandler} for JSON payloads
*
* @author Andy Wilkinson
*/
class JsonPayloadHandler implements PayloadHandler {
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
private final ObjectMapper objectMapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
private final String rawPayload;
JsonPayloadHandler(String payload) throws IOException {
this.rawPayload = payload;
}
@Override
public List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors) {
List<FieldDescriptor> missingFields = new ArrayList<FieldDescriptor>();
Object payload = readPayload();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
if (!fieldDescriptor.isOptional()
&& !this.fieldProcessor.hasField(
JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) {
missingFields.add(fieldDescriptor);
}
}
return missingFields;
}
@Override
public String getUndocumentedPayload(List<FieldDescriptor> fieldDescriptors) {
Object payload = readPayload();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath());
this.fieldProcessor.remove(path, payload);
}
if (!isEmpty(payload)) {
try {
return this.objectMapper.writeValueAsString(payload);
}
catch (JsonProcessingException ex) {
throw new PayloadHandlingException(ex);
}
}
return null;
}
private Object readPayload() {
try {
return new ObjectMapper().readValue(this.rawPayload, Object.class);
}
catch (IOException ex) {
throw new PayloadHandlingException(ex);
}
}
private boolean isEmpty(Object object) {
if (object instanceof Map) {
return ((Map<?, ?>) object).isEmpty();
}
return ((List<?>) object).isEmpty();
}
@Override
public Object determineFieldType(String path) {
try {
return new JsonFieldTypeResolver().resolveFieldType(path, readPayload());
}
catch (FieldDoesNotExistException ex) {
String message = "Cannot determine the type of the field '" + path + "' as"
+ " it is not present in the payload. Please provide a type using"
+ " FieldDescriptor.type(Object type).";
throw new FieldTypeRequiredException(message);
}
}
}

View File

@@ -37,8 +37,12 @@ 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:
* When documenting an XML payload, the {@code path} uses XPath, i.e. '/' is used to
* descend to a child node.
* <p>
* When documenting a JSON payload, the {@code path} uses '.' to descend into a child
* object and ' {@code []}' to descend into an array. For example, with this JSON
* payload:
*
* <pre>
* {
@@ -132,8 +136,7 @@ public abstract class PayloadDocumentation {
*/
public static Snippet requestFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
return new RequestFieldsSnippet(attributes,
Arrays.asList(descriptors));
return new RequestFieldsSnippet(attributes, Arrays.asList(descriptors));
}
/**
@@ -174,8 +177,7 @@ public abstract class PayloadDocumentation {
*/
public static Snippet responseFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
return new ResponseFieldsSnippet(attributes,
Arrays.asList(descriptors));
return new ResponseFieldsSnippet(attributes, Arrays.asList(descriptors));
}
}

View File

@@ -0,0 +1,45 @@
package org.springframework.restdocs.payload;
import java.util.List;
/**
* A handler for a request or response payload
*
* @author Andy Wilkinson
*/
interface PayloadHandler {
/**
* Finds the fields that are missing from the handler's payload. A field is missing if
* it is described by one of the {@code fieldDescriptors} but is not present in the
* payload.
*
* @param fieldDescriptors the descriptors
* @return descriptors for the fields that are missing from the payload
* @throws PayloadHandlingException if a failure occurs
*/
List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors);
/**
* Returns a modified payload, formatted as a String, that only contains the fields
* that are undocumented. A field is undocumented if it is present in the handler's
* payload but is not described by the given {@code fieldDescriptors}. If the payload
* is completely documented, {@code null} is returned
*
* @param fieldDescriptors the descriptors
* @return the undocumented payload, or {@code null} if all of the payload is
* documented
* @throws PayloadHandlingException if a failure occurs
*/
String getUndocumentedPayload(List<FieldDescriptor> fieldDescriptors);
/**
* Returns the type of the field with the given {@code path} based on the content of
* the payload.
*
* @param path the field path
* @return the type of the field
*/
Object determineFieldType(String path);
}

View File

@@ -0,0 +1,20 @@
package org.springframework.restdocs.payload;
/**
* Thrown to indicate that a failure has occurred during payload handling
*
* @author Andy Wilkinson
*
*/
@SuppressWarnings("serial")
class PayloadHandlingException extends RuntimeException {
/**
* Creates a new {@code PayloadHandlingException} with the given cause
* @param cause the cause of the failure
*/
PayloadHandlingException(Throwable cause) {
super(cause);
}
}

View File

@@ -20,6 +20,7 @@ import java.io.Reader;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult;
@@ -43,4 +44,13 @@ class RequestFieldsSnippet extends AbstractFieldsSnippet {
return result.getRequest().getReader();
}
@Override
protected MediaType getContentType(MvcResult result) {
String contentType = result.getRequest().getContentType();
if (contentType != null) {
return MediaType.valueOf(contentType);
}
return null;
}
}

View File

@@ -21,11 +21,12 @@ import java.io.StringReader;
import java.util.List;
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult;
/**
* A {@link Snippet} the documents the fields in a response.
* A {@link Snippet} that documents the fields in a response.
*
* @author Andy Wilkinson
*/
@@ -45,4 +46,13 @@ class ResponseFieldsSnippet extends AbstractFieldsSnippet {
return new StringReader(result.getResponse().getContentAsString());
}
@Override
protected MediaType getContentType(MvcResult result) {
String contentType = result.getResponse().getContentType();
if (contentType != null) {
return MediaType.valueOf(contentType);
}
return null;
}
}

View File

@@ -0,0 +1,141 @@
package org.springframework.restdocs.payload;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
/**
* A {@link PayloadHandler} for XML payloads
*
* @author Andy Wilkinson
*/
class XmlPayloadHandler implements PayloadHandler {
private final DocumentBuilder documentBuilder;
private final String rawPayload;
XmlPayloadHandler(String rawPayload) {
try {
this.documentBuilder = DocumentBuilderFactory.newInstance()
.newDocumentBuilder();
}
catch (ParserConfigurationException ex) {
throw new IllegalStateException("Failed to create document builder", ex);
}
this.rawPayload = rawPayload;
}
@Override
public List<FieldDescriptor> findMissingFields(List<FieldDescriptor> fieldDescriptors) {
List<FieldDescriptor> missingFields = new ArrayList<>();
Document payload = readPayload();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
if (!fieldDescriptor.isOptional()) {
NodeList matchingNodes = findMatchingNodes(fieldDescriptor, payload);
if (matchingNodes.getLength() == 0) {
missingFields.add(fieldDescriptor);
}
}
}
return missingFields;
}
private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document payload) {
try {
return (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload,
XPathConstants.NODESET);
}
catch (XPathExpressionException ex) {
throw new PayloadHandlingException(ex);
}
}
private Document readPayload() {
try {
return this.documentBuilder.parse(new InputSource(new StringReader(
this.rawPayload)));
}
catch (Exception ex) {
throw new PayloadHandlingException(ex);
}
}
private XPathExpression createXPath(String fieldPath) throws XPathExpressionException {
return XPathFactory.newInstance().newXPath().compile(fieldPath);
}
@Override
public String getUndocumentedPayload(List<FieldDescriptor> fieldDescriptors) {
Document payload = readPayload();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
NodeList matchingNodes;
try {
matchingNodes = (NodeList) createXPath(fieldDescriptor.getPath())
.evaluate(payload, XPathConstants.NODESET);
}
catch (XPathExpressionException ex) {
throw new PayloadHandlingException(ex);
}
for (int i = 0; i < matchingNodes.getLength(); i++) {
Node node = matchingNodes.item(i);
node.getParentNode().removeChild(node);
}
}
if (payload.getChildNodes().getLength() > 0) {
return prettyPrint(payload);
}
return null;
}
private String prettyPrint(Document document) {
try {
StringWriter stringWriter = new StringWriter();
StreamResult xmlOutput = new StreamResult(stringWriter);
TransformerFactory transformerFactory = TransformerFactory.newInstance();
transformerFactory.setAttribute("indent-number", 4);
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
transformer.transform(new DOMSource(document), xmlOutput);
return xmlOutput.getWriter().toString();
}
catch (Exception ex) {
throw new PayloadHandlingException(ex);
}
}
@Override
public Object determineFieldType(String path) {
try {
return new JsonFieldTypeResolver().resolveFieldType(path, readPayload());
}
catch (FieldDoesNotExistException ex) {
String message = "Cannot determine the type of the field '" + path + "' as"
+ " it is not present in the payload. Please provide a type using"
+ " FieldDescriptor.type(Object type).";
throw new FieldTypeRequiredException(message);
}
}
}

View File

@@ -1,114 +0,0 @@
/*
* 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 java.io.IOException;
import java.io.StringReader;
import java.util.Arrays;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.restdocs.snippet.SnippetException;
/**
* Tests for {@link FieldValidator}
*
* @author Andy Wilkinson
*/
public class FieldValidatorTests {
private final FieldValidator fieldValidator = new FieldValidator();
@Rule
public ExpectedException thrownException = ExpectedException.none();
private StringReader listPayload = new StringReader(
"[{\"a\":1},{\"a\":2},{\"b\":{\"c\":3}}]");
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.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("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"), new FieldDescriptor("a.d[].e")));
}
@Test
public void childIsDocumentedWhenParentIsDocumented() throws IOException {
this.fieldValidator.validate(this.payload,
Arrays.asList(new FieldDescriptor("a")));
}
@Test
public void missingField() throws IOException {
this.thrownException.expect(SnippetException.class);
this.thrownException
.expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [y, z]"));
this.fieldValidator.validate(this.payload, Arrays.asList(
new FieldDescriptor("a"), new FieldDescriptor("a.b"),
new FieldDescriptor("y"), new FieldDescriptor("z")));
}
@Test
public void undocumentedField() throws IOException {
this.thrownException.expect(SnippetException.class);
this.thrownException.expectMessage(equalTo(String
.format("The following parts 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"), new FieldDescriptor("a.d")));
}
@Test
public void listPayloadNoMissingFieldsAllFieldsDocumented() throws IOException {
this.fieldValidator.validate(this.listPayload, Arrays.asList(new FieldDescriptor(
"[]b.c"), new FieldDescriptor("[]b"), new FieldDescriptor("[]a"),
new FieldDescriptor("[]")));
}
@Test
public void listPayloadParentIsDocumentedWhenAllChildrenAreDocumented()
throws IOException {
this.fieldValidator.validate(this.listPayload,
Arrays.asList(new FieldDescriptor("[]b.c"), new FieldDescriptor("[]a")));
}
@Test
public void listPayloadChildIsDocumentedWhenParentIsDocumented() throws IOException {
this.fieldValidator.validate(this.listPayload,
Arrays.asList(new FieldDescriptor("[]")));
}
}

View File

@@ -24,89 +24,89 @@ import static org.junit.Assert.assertTrue;
import org.junit.Test;
/**
* Tests for {@link FieldPath}
* Tests for {@link JsonFieldPath}
*
* @author Andy Wilkinson
*/
public class FieldPathTests {
public class JsonFieldPathTests {
@Test
public void singleFieldIsPrecise() {
assertTrue(FieldPath.compile("a").isPrecise());
assertTrue(JsonFieldPath.compile("a").isPrecise());
}
@Test
public void singleNestedFieldIsPrecise() {
assertTrue(FieldPath.compile("a.b").isPrecise());
assertTrue(JsonFieldPath.compile("a.b").isPrecise());
}
@Test
public void topLevelArrayIsNotPrecise() {
assertFalse(FieldPath.compile("[]").isPrecise());
assertFalse(JsonFieldPath.compile("[]").isPrecise());
}
@Test
public void fieldBeneathTopLevelArrayIsNotPrecise() {
assertFalse(FieldPath.compile("[]a").isPrecise());
assertFalse(JsonFieldPath.compile("[]a").isPrecise());
}
@Test
public void arrayIsNotPrecise() {
assertFalse(FieldPath.compile("a[]").isPrecise());
assertFalse(JsonFieldPath.compile("a[]").isPrecise());
}
@Test
public void nestedArrayIsNotPrecise() {
assertFalse(FieldPath.compile("a.b[]").isPrecise());
assertFalse(JsonFieldPath.compile("a.b[]").isPrecise());
}
@Test
public void arrayOfArraysIsNotPrecise() {
assertFalse(FieldPath.compile("a[][]").isPrecise());
assertFalse(JsonFieldPath.compile("a[][]").isPrecise());
}
@Test
public void fieldBeneathAnArrayIsNotPrecise() {
assertFalse(FieldPath.compile("a[].b").isPrecise());
assertFalse(JsonFieldPath.compile("a[].b").isPrecise());
}
@Test
public void compilationOfSingleElementPath() {
assertThat(FieldPath.compile("a").getSegments(), contains("a"));
assertThat(JsonFieldPath.compile("a").getSegments(), contains("a"));
}
@Test
public void compilationOfMultipleElementPath() {
assertThat(FieldPath.compile("a.b.c").getSegments(), contains("a", "b", "c"));
assertThat(JsonFieldPath.compile("a.b.c").getSegments(), contains("a", "b", "c"));
}
@Test
public void compilationOfPathWithArraysWithNoDotSeparators() {
assertThat(FieldPath.compile("a[]b[]c").getSegments(),
assertThat(JsonFieldPath.compile("a[]b[]c").getSegments(),
contains("a", "[]", "b", "[]", "c"));
}
@Test
public void compilationOfPathWithArraysWithPreAndPostDotSeparators() {
assertThat(FieldPath.compile("a.[].b.[].c").getSegments(),
assertThat(JsonFieldPath.compile("a.[].b.[].c").getSegments(),
contains("a", "[]", "b", "[]", "c"));
}
@Test
public void compilationOfPathWithArraysWithPreDotSeparators() {
assertThat(FieldPath.compile("a.[]b.[]c").getSegments(),
assertThat(JsonFieldPath.compile("a.[]b.[]c").getSegments(),
contains("a", "[]", "b", "[]", "c"));
}
@Test
public void compilationOfPathWithArraysWithPostDotSeparators() {
assertThat(FieldPath.compile("a[].b[].c").getSegments(),
assertThat(JsonFieldPath.compile("a[].b[].c").getSegments(),
contains("a", "[]", "b", "[]", "c"));
}
@Test
public void compilationOfPathStartingWithAnArray() {
assertThat(FieldPath.compile("[]a.b.c").getSegments(),
assertThat(JsonFieldPath.compile("[]a.b.c").getSegments(),
contains("[]", "a", "b", "c"));
}

View File

@@ -30,19 +30,19 @@ import org.junit.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Tests for {@link FieldProcessor}
* Tests for {@link JsonFieldProcessor}
*
* @author Andy Wilkinson
*/
public class FieldProcessorTests {
public class JsonFieldProcessorTests {
private final FieldProcessor fieldProcessor = new FieldProcessor();
private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor();
@Test
public void extractTopLevelMapEntry() {
Map<String, Object> payload = new HashMap<>();
payload.put("a", "alpha");
assertThat(this.fieldProcessor.extract(FieldPath.compile("a"), payload),
assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a"), payload),
equalTo((Object) "alpha"));
}
@@ -52,7 +52,7 @@ public class FieldProcessorTests {
Map<String, Object> alpha = new HashMap<>();
payload.put("a", alpha);
alpha.put("b", "bravo");
assertThat(this.fieldProcessor.extract(FieldPath.compile("a.b"), payload),
assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload),
equalTo((Object) "bravo"));
}
@@ -63,7 +63,7 @@ public class FieldProcessorTests {
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),
assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a"), payload),
equalTo((Object) alpha));
}
@@ -74,7 +74,7 @@ public class FieldProcessorTests {
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),
assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload),
equalTo((Object) alpha));
}
@@ -85,7 +85,7 @@ public class FieldProcessorTests {
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),
assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[].b"), payload),
equalTo((Object) Arrays.asList("bravo", "bravo")));
}
@@ -98,7 +98,7 @@ public class FieldProcessorTests {
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),
assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][]"), payload),
equalTo((Object) Arrays.asList(entry1, entry2, entry3)));
}
@@ -111,7 +111,7 @@ public class FieldProcessorTests {
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),
assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].id"), payload),
equalTo((Object) Arrays.asList("1", "2", "3")));
}
@@ -124,7 +124,7 @@ public class FieldProcessorTests {
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),
assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), payload),
equalTo((Object) Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3),
Arrays.asList(4))));
}
@@ -132,21 +132,21 @@ public class FieldProcessorTests {
@Test(expected = FieldDoesNotExistException.class)
public void nonExistentTopLevelField() {
this.fieldProcessor
.extract(FieldPath.compile("a"), new HashMap<String, Object>());
.extract(JsonFieldPath.compile("a"), new HashMap<String, Object>());
}
@Test(expected = FieldDoesNotExistException.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);
this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload);
}
@Test(expected = FieldDoesNotExistException.class)
public void nonExistentNestedFieldWhenParentIsNotAMap() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put("a", 5);
this.fieldProcessor.extract(FieldPath.compile("a.b"), payload);
this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload);
}
@Test(expected = FieldDoesNotExistException.class)
@@ -155,20 +155,20 @@ public class FieldProcessorTests {
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);
this.fieldProcessor.extract(JsonFieldPath.compile("a.b.c"), payload);
}
@Test(expected = FieldDoesNotExistException.class)
public void nonExistentArrayField() {
HashMap<String, Object> payload = new HashMap<String, Object>();
this.fieldProcessor.extract(FieldPath.compile("a[]"), payload);
this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload);
}
@Test(expected = FieldDoesNotExistException.class)
public void nonExistentArrayFieldAsTypeDoesNotMatch() {
HashMap<String, Object> payload = new HashMap<String, Object>();
payload.put("a", 5);
this.fieldProcessor.extract(FieldPath.compile("a[]"), payload);
this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload);
}
@Test(expected = FieldDoesNotExistException.class)
@@ -177,14 +177,14 @@ public class FieldProcessorTests {
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);
this.fieldProcessor.extract(JsonFieldPath.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);
this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload);
assertThat(payload.size(), equalTo(0));
}
@@ -194,7 +194,7 @@ public class FieldProcessorTests {
Map<String, Object> alpha = new HashMap<>();
payload.put("a", alpha);
alpha.put("b", "bravo");
this.fieldProcessor.remove(FieldPath.compile("a.b"), payload);
this.fieldProcessor.remove(JsonFieldPath.compile("a.b"), payload);
assertThat(payload.size(), equalTo(0));
}
@@ -203,7 +203,7 @@ public class FieldProcessorTests {
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);
this.fieldProcessor.remove(JsonFieldPath.compile("a[].b"), payload);
assertThat(payload.size(), equalTo(0));
}
@@ -212,7 +212,7 @@ public class FieldProcessorTests {
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);
this.fieldProcessor.remove(JsonFieldPath.compile("a[][].id"), payload);
assertThat(payload.size(), equalTo(0));
}

View File

@@ -29,66 +29,66 @@ import org.junit.rules.ExpectedException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Tests for {@link FieldTypeResolver}
* Tests for {@link JsonFieldTypeResolver}
*
* @author Andy Wilkinson
*
*/
public class FieldTypeResolverTests {
public class JsonFieldTypeResolverTests {
private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver();
private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver();
@Rule
public ExpectedException thrownException = ExpectedException.none();
@Test
public void arrayField() throws IOException {
assertFieldType(FieldType.ARRAY, "[]");
assertFieldType(JsonFieldType.ARRAY, "[]");
}
@Test
public void booleanField() throws IOException {
assertFieldType(FieldType.BOOLEAN, "true");
assertFieldType(JsonFieldType.BOOLEAN, "true");
}
@Test
public void objectField() throws IOException {
assertFieldType(FieldType.OBJECT, "{}");
assertFieldType(JsonFieldType.OBJECT, "{}");
}
@Test
public void nullField() throws IOException {
assertFieldType(FieldType.NULL, "null");
assertFieldType(JsonFieldType.NULL, "null");
}
@Test
public void numberField() throws IOException {
assertFieldType(FieldType.NUMBER, "1.2345");
assertFieldType(JsonFieldType.NUMBER, "1.2345");
}
@Test
public void stringField() throws IOException {
assertFieldType(FieldType.STRING, "\"Foo\"");
assertFieldType(JsonFieldType.STRING, "\"Foo\"");
}
@Test
public void nestedField() throws IOException {
assertThat(this.fieldTypeResolver.resolveFieldType("a.b.c",
createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(FieldType.OBJECT));
createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(JsonFieldType.OBJECT));
}
@Test
public void multipleFieldsWithSameType() throws IOException {
assertThat(this.fieldTypeResolver.resolveFieldType("a[].id",
createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")),
equalTo(FieldType.NUMBER));
equalTo(JsonFieldType.NUMBER));
}
@Test
public void multipleFieldsWithDifferentTypes() throws IOException {
assertThat(this.fieldTypeResolver.resolveFieldType("a[].id",
createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")),
equalTo(FieldType.VARIES));
equalTo(JsonFieldType.VARIES));
}
@Test
@@ -99,7 +99,7 @@ public class FieldTypeResolverTests {
this.fieldTypeResolver.resolveFieldType("a.b", createPayload("{\"a\":{}}"));
}
private void assertFieldType(FieldType expectedType, String jsonValue)
private void assertFieldType(JsonFieldType expectedType, String jsonValue)
throws IOException {
assertThat(this.fieldTypeResolver.resolveFieldType("field",
createSimplePayload(jsonValue)), equalTo(expectedType));

View File

@@ -36,6 +36,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.snippet.SnippetException;
@@ -65,8 +66,8 @@ public class RequestFieldsSnippetTests {
.row("a.c", "String", "two") //
.row("a", "Object", "three"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
.description("one"), fieldWithPath("a.c").description("two"),
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"),
fieldWithPath("a.c").description("two"),
fieldWithPath("a").description("three"))).document(
"map-request-with-fields",
result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}")));
@@ -80,9 +81,9 @@ public class RequestFieldsSnippetTests {
.row("[]a.c", "String", "two") //
.row("[]a", "Object", "three"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b")
.description("one"), fieldWithPath("[]a.c").description("two"),
fieldWithPath("[]a").description("three"))).document(
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"),
fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a")
.description("three"))).document(
"array-request-with-fields",
result(get("/foo").content(
"[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]")));
@@ -94,9 +95,8 @@ public class RequestFieldsSnippetTests {
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
new RequestFieldsSnippet(Collections.<FieldDescriptor> emptyList())
.document("undocumented-request-field",
result(get("/foo").content("{\"a\": 5}")));
new RequestFieldsSnippet(Collections.<FieldDescriptor> emptyList()).document(
"undocumented-request-field", result(get("/foo").content("{\"a\": 5}")));
}
@Test
@@ -105,18 +105,16 @@ public class RequestFieldsSnippetTests {
this.thrown
.expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
.description("one"))).document("missing-request-fields",
result(get("/foo").content("{}")));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")))
.document("missing-request-fields", result(get("/foo").content("{}")));
}
@Test
public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException {
this.thrown.expect(FieldTypeRequiredException.class);
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
.description("one").optional())).document(
"missing-optional-request-field-with-no-type", result(get("/foo")
.content("{ }")));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")
.optional())).document("missing-optional-request-field-with-no-type",
result(get("/foo").content("{ }")));
}
@Test
@@ -128,10 +126,9 @@ public class RequestFieldsSnippetTests {
this.thrown
.expectMessage(endsWith("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
.description("one"))).document(
"undocumented-request-field-and-missing-request-field",
result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one")))
.document("undocumented-request-field-and-missing-request-field",
result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
}
@Test
@@ -171,10 +168,74 @@ public class RequestFieldsSnippetTests {
resolver));
request.setContent("{\"a\": \"foo\"}".getBytes());
MockHttpServletResponse response = new MockHttpServletResponse();
new RequestFieldsSnippet(attributes(key("title").value(
"Custom title")), Arrays.asList(fieldWithPath("a").description("one")))
.document("request-fields-with-custom-attributes",
result(request, response));
new RequestFieldsSnippet(attributes(key("title").value("Custom title")),
Arrays.asList(fieldWithPath("a").description("one"))).document(
"request-fields-with-custom-attributes", result(request, response));
}
@Test
public void xmlRequestFields() throws IOException {
this.snippet.expectRequestFields("xml-request").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("a/b", "b", "one") //
.row("a/c", "c", "two") //
.row("a", "a", "three"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")
.type("b"), fieldWithPath("a/c").description("two").type("c"),
fieldWithPath("a").description("three").type("a"))).document(
"xml-request",
result(get("/foo").content("<a><b>5</b><c>charlie</c></a>").contentType(
MediaType.APPLICATION_XML)));
}
@Test
public void undocumentedXmlRequestField() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
new RequestFieldsSnippet(Collections.<FieldDescriptor> emptyList()).document(
"undocumented-xml-request-field",
result(get("/foo").content("<a><b>5</b></a>").contentType(
MediaType.APPLICATION_XML)));
}
@Test
public void xmlRequestFieldWithNoType() throws IOException {
this.thrown.expect(FieldTypeRequiredException.class);
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")))
.document("missing-xml-request",
result(get("/foo").contentType(MediaType.APPLICATION_XML)
.content("<a>5</a>")));
}
@Test
public void missingXmlRequestField() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [a/b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"),
fieldWithPath("a").description("one"))).document(
"missing-xml-request-fields",
result(get("/foo").contentType(MediaType.APPLICATION_XML).content(
"<a></a>")));
}
@Test
public void undocumentedXmlRequestFieldAndMissingXmlRequestField() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
this.thrown
.expectMessage(endsWith("Fields with the following paths were not found"
+ " in the payload: [a/b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")))
.document("undocumented-xml-request-field-and-missing-xml-request-field",
result(get("/foo").contentType(MediaType.APPLICATION_XML)
.content("<a><c>5</c></a>")));
}
private FileSystemResource snippetResource(String name) {

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.restdocs.payload;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -26,13 +28,16 @@ import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
@@ -66,10 +71,10 @@ public class ResponseFieldsSnippetTests {
response.getWriter().append(
"{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":"
+ " [{\"id\":356,\"name\": \"sample\"}]}");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id")
.description("one"), fieldWithPath("date").description("two"),
fieldWithPath("assets").description("three"), fieldWithPath("assets[]")
.description("four"),
new ResponseFieldsSnippet(Arrays.asList(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"))).document(
"map-response-with-fields", result(response));
@@ -86,10 +91,10 @@ public class ResponseFieldsSnippetTests {
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter()
.append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b")
.description("one"), fieldWithPath("[]a.c").description("two"),
fieldWithPath("[]a").description("three"))).document(
"array-response-with-fields", result(response));
new ResponseFieldsSnippet(Arrays.asList(
fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c")
.description("two"), fieldWithPath("[]a").description("three")))
.document("array-response-with-fields", result(response));
}
@Test
@@ -100,8 +105,8 @@ public class ResponseFieldsSnippetTests {
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("[\"a\", \"b\", \"c\"]");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]")
.description("one"))).document("array-response", result(response));
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one")))
.document("array-response", result(response));
}
@Test
@@ -142,10 +147,80 @@ public class ResponseFieldsSnippetTests {
resolver));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getOutputStream().print("{\"a\": \"foo\"}");
new ResponseFieldsSnippet(attributes(key("title").value(
"Custom title")), Arrays.asList(fieldWithPath("a").description("one")))
.document("response-fields-with-custom-attributes",
result(request, response));
new ResponseFieldsSnippet(attributes(key("title").value("Custom title")),
Arrays.asList(fieldWithPath("a").description("one"))).document(
"response-fields-with-custom-attributes", result(request, response));
}
@Test
public void xmlResponseFields() throws IOException {
this.snippet.expectResponseFields("xml-response").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("a/b", "b", "one") //
.row("a/c", "c", "two") //
.row("a", "a", "three"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a><b>5</b><c>charlie</c></a>");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")
.type("b"), fieldWithPath("a/c").description("two").type("c"),
fieldWithPath("a").description("three").type("a"))).document(
"xml-response", result(response));
}
@Test
public void undocumentedXmlResponseField() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a><b>5</b></a>");
new ResponseFieldsSnippet(Collections.<FieldDescriptor> emptyList()).document(
"undocumented-xml-response-field", result(response));
}
@Test
public void xmlResponseFieldWithNoType() throws IOException {
this.thrown.expect(FieldTypeRequiredException.class);
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a>5</a>");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")))
.document("xml-response-no-field-type", result(response));
}
@Test
public void missingXmlResponseField() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [a/b]"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a></a>");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"),
fieldWithPath("a").description("one"))).document(
"missing-xml-response-field", result(response));
}
@Test
public void undocumentedXmlResponseFieldAndMissingXmlResponseField()
throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
this.thrown
.expectMessage(endsWith("Fields with the following paths were not found"
+ " in the payload: [a/b]"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_XML_VALUE);
response.getOutputStream().print("<a><c>5</c></a>");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one")))
.document("undocumented-xml-request-field-and-missing-xml-request-field",
result(response));
}
private FileSystemResource snippetResource(String name) {