diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java
index 571f2bec..bcf3675d 100644
--- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java
@@ -20,6 +20,8 @@ import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRe
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks;
+import static org.springframework.restdocs.state.StateDocumentation.documentRequestFields;
+import static org.springframework.restdocs.state.StateDocumentation.documentResponseFields;
import java.util.ArrayList;
import java.util.List;
@@ -28,6 +30,9 @@ import org.springframework.restdocs.hypermedia.HypermediaDocumentation;
import org.springframework.restdocs.hypermedia.LinkDescriptor;
import org.springframework.restdocs.hypermedia.LinkExtractor;
import org.springframework.restdocs.hypermedia.LinkExtractors;
+import org.springframework.restdocs.state.FieldDescriptor;
+import org.springframework.restdocs.state.Path;
+import org.springframework.restdocs.state.StateDocumentation;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
@@ -35,6 +40,7 @@ import org.springframework.test.web.servlet.ResultHandler;
* A Spring MVC Test {@code ResultHandler} for documenting RESTful APIs.
*
* @author Andy Wilkinson
+ * @author Andreas Evers
* @see RestDocumentation#document(String)
*/
public class RestDocumentationResultHandler implements ResultHandler {
@@ -97,4 +103,42 @@ public class RestDocumentationResultHandler implements ResultHandler {
return this;
}
+ /**
+ * Document the fields in the response using the given {@code descriptors}. The fields
+ * are extracted from the response based on its content type.
+ *
+ * If a field is present in the response but is not described by one of the
+ * descriptors a failure will occur when this handler is invoked. Similarly, if a
+ * field is described but is not present in the response a failure will also occur
+ * when this handler is invoked.
+ *
+ * @param descriptors the link descriptors
+ * @return {@code this}
+ * @see StateDocumentation#fieldWithPath(Path)
+ */
+ public RestDocumentationResultHandler withRequestFields(
+ FieldDescriptor... descriptors) {
+ this.delegates.add(documentRequestFields(this.outputDir, descriptors));
+ return this;
+ }
+
+ /**
+ * Document the fields in the response using the given {@code descriptors}. The fields
+ * are extracted from the response based on its content type.
+ *
+ * If a field is present in the response but is not described by one of the
+ * descriptors a failure will occur when this handler is invoked. Similarly, if a
+ * field is described but is not present in the response a failure will also occur
+ * when this handler is invoked.
+ *
+ * @param descriptors the link descriptors
+ * @return {@code this}
+ * @see StateDocumentation#fieldWithPath(Path)
+ */
+ public RestDocumentationResultHandler withResponseFields(
+ FieldDescriptor... descriptors) {
+ this.delegates.add(documentResponseFields(this.outputDir, descriptors));
+ return this;
+ }
+
}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java
index 65a51bd0..2393fa62 100644
--- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java
@@ -59,5 +59,4 @@ public abstract class HypermediaDocumentation {
return new LinkSnippetResultHandler(outputDir, linkExtractor,
Arrays.asList(descriptors));
}
-
}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/Field.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/Field.java
new file mode 100644
index 00000000..e0b124fe
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/Field.java
@@ -0,0 +1,95 @@
+/*
+ * 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.state;
+
+import org.springframework.core.style.ToStringCreator;
+
+/**
+ * Representation of a field used in a Hypermedia-based API
+ *
+ * @author Andreas Evers
+ */
+public class Field {
+
+ private final Path path;
+
+ private final Object value;
+
+ /**
+ * Creates a new {@code Field} with the given {@code path} and {@code value}
+ *
+ * @param path The field's path
+ * @param value The field's value
+ */
+ public Field(Path path, Object value) {
+ this.path = path;
+ this.value = value;
+ }
+
+ /**
+ * Returns the field's {@code path}
+ * @return the field's {@code path}
+ */
+ public Path getPath() {
+ return this.path;
+ }
+
+ /**
+ * Returns the field's {@code value}
+ * @return the field's {@code value}
+ */
+ public Object getValue() {
+ return this.value;
+ }
+
+ @Override
+ public int hashCode() {
+ int prime = 31;
+ int result = 1;
+ result = prime * result + this.path.hashCode();
+ result = prime * result + this.value.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Field other = (Field) obj;
+ if (!this.path.equals(other.path)) {
+ return false;
+ }
+ if (!this.value.equals(other.value)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this).append("path", this.path)
+ .append("value", this.value).toString();
+ }
+
+}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldDescriptor.java
new file mode 100644
index 00000000..8b9cc9ff
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldDescriptor.java
@@ -0,0 +1,105 @@
+/*
+ * 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.state;
+
+/**
+ * A description of a field found in a hypermedia API
+ *
+ * @see StateDocumentation#fieldWithPath(Path)
+ *
+ * @author Andreas Evers
+ */
+public class FieldDescriptor {
+
+ private final Path path;
+
+ private String type;
+
+ private boolean required;
+
+ private String constraints;
+
+ private String description;
+
+ FieldDescriptor(Path path) {
+ this.path = path;
+ }
+
+ /**
+ * Specifies the type of the field
+ *
+ * @param type The field's type (could be number, string, boolean, array, object, ...)
+ * @return {@code this}
+ */
+ public FieldDescriptor type(String type) {
+ this.type = type;
+ return this;
+ }
+
+ /**
+ * Specifies necessity of the field
+ *
+ * @param required The field's necessity
+ * @return {@code this}
+ */
+ public FieldDescriptor required(boolean required) {
+ this.required = required;
+ return this;
+ }
+
+ /**
+ * Specifies the constraints of the field
+ *
+ * @param constraints The field's constraints
+ * @return {@code this}
+ */
+ public FieldDescriptor constraints(String constraints) {
+ this.constraints = constraints;
+ return this;
+ }
+
+ /**
+ * Specifies the description of the field
+ *
+ * @param description The field's description
+ * @return {@code this}
+ */
+ public FieldDescriptor description(String description) {
+ this.description = description;
+ return this;
+ }
+
+ Path getPath() {
+ return this.path;
+ }
+
+ String getType() {
+ return this.type;
+ }
+
+ boolean isRequired() {
+ return this.required;
+ }
+
+ String getConstraints() {
+ return this.constraints;
+ }
+
+ String getDescription() {
+ return this.description;
+ }
+}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldExtractor.java
new file mode 100644
index 00000000..a46b2f87
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldExtractor.java
@@ -0,0 +1,86 @@
+/*
+ * 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.state;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.util.Assert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * A {@code FieldExtractor} is used to extract {@link Field fields} from a JSON response.
+ * The expected format of the links in the response is determined by the implementation.
+ *
+ * @author Andy Wilkinson
+ *
+ */
+public class FieldExtractor {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ private Map extractedFields = new HashMap<>();
+
+ @SuppressWarnings("unchecked")
+ public Map extractFields(MockHttpServletRequest request)
+ throws IOException {
+ Map jsonContent = this.objectMapper.readValue(
+ request.getInputStream(), Map.class);
+ extractFieldsRecursively(jsonContent);
+ return this.extractedFields;
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map extractFields(MockHttpServletResponse response)
+ throws IOException {
+ String responseBody = response.getContentAsString();
+ Assert.hasText(responseBody,
+ "The response doesn't contain a body to extract fields from");
+ Map jsonContent = this.objectMapper.readValue(responseBody,
+ Map.class);
+ extractFieldsRecursively(jsonContent);
+ return this.extractedFields;
+ }
+
+ private void extractFieldsRecursively(Map jsonContent) {
+ extractFieldsRecursively(null, jsonContent);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void extractFieldsRecursively(Path previousSteps,
+ Map jsonContent) {
+ for (Entry entry : jsonContent.entrySet()) {
+ Path path;
+ if (previousSteps == null) {
+ path = new Path(entry.getKey());
+ }
+ else {
+ path = new Path(previousSteps, entry.getKey());
+ }
+ this.extractedFields.put(path, new Field(path, entry.getValue()));
+ if (entry.getValue() instanceof Map) {
+ Map value = (Map) entry.getValue();
+ extractFieldsRecursively(path, value);
+ }
+ }
+ }
+}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldSnippetResultHandler.java
new file mode 100644
index 00000000..b77ae64d
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldSnippetResultHandler.java
@@ -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.state;
+
+import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.REQUEST;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.springframework.restdocs.snippet.DocumentationWriter;
+import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful
+ * resource's request or response fields.
+ *
+ * @author Andreas Evers
+ */
+public class FieldSnippetResultHandler extends SnippetWritingResultHandler {
+
+ private final Map descriptorsByName = new HashMap();
+
+ private final Type type;
+
+ private FieldExtractor extractor = new FieldExtractor();
+
+ private StateDocumentationValidator validator;
+
+ enum Type {
+ REQUEST, RESPONSE;
+ }
+
+ FieldSnippetResultHandler(String outputDir, FieldSnippetResultHandler.Type type,
+ List descriptors) {
+ super(outputDir, type.toString().toLowerCase() + "fields");
+ this.type = type;
+ for (FieldDescriptor descriptor : descriptors) {
+ Assert.notNull(descriptor.getPath());
+ Assert.hasText(descriptor.getDescription());
+ this.descriptorsByName.put(descriptor.getPath(), descriptor);
+ }
+ this.validator = new StateDocumentationValidator(type);
+ }
+
+ @Override
+ protected void handle(MvcResult result, DocumentationWriter writer)
+ throws IOException {
+ Map fields;
+ if (this.type == REQUEST) {
+ fields = this.extractor.extractFields(result.getRequest());
+ }
+ else {
+ fields = this.extractor.extractFields(result.getResponse());
+ }
+
+ SortedSet actualFields = new TreeSet(fields.keySet());
+ SortedSet expectedFields = new TreeSet(
+ this.descriptorsByName.keySet());
+
+ this.validator.validateFields(actualFields, expectedFields);
+
+ writer.println("|===");
+ writer.println("| Path | Description | Type | Required | Constraints");
+
+ for (Entry entry : this.descriptorsByName.entrySet()) {
+ writer.println();
+ writer.println("| " + entry.getKey());
+ writer.println("| " + entry.getValue().getDescription());
+ writer.println("| " + entry.getValue().getType());
+ writer.println("| " + entry.getValue().isRequired());
+ writer.println("| " + entry.getValue().getConstraints());
+ }
+
+ writer.println("|===");
+ }
+
+}
\ No newline at end of file
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/Path.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/Path.java
new file mode 100644
index 00000000..a82557ac
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/Path.java
@@ -0,0 +1,124 @@
+/*
+ * 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.state;
+
+import static java.lang.Math.min;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.core.style.ToStringCreator;
+
+/**
+ * Representation of a path for a field. In case the field is a nested field, there will
+ * be multiple steps. Each step is the name of a field, albeit parents of the field in
+ * question. In case the field is not nested, there is only one step, which is the name of the field.
+ *
+ * @author Andreas Evers
+ */
+public class Path implements Comparable {
+
+ private List steps = new ArrayList<>();
+
+ public Path(Path path) {
+ this.steps = new ArrayList(path.getSteps());
+ }
+
+ public Path(List steps) {
+ this.steps = steps;
+ }
+
+ public Path(Path previousSteps, String newStep) {
+ this.steps.addAll(previousSteps.getSteps());
+ this.steps.add(newStep);
+ }
+
+ public Path(String... steps) {
+ this.steps = Arrays.asList(steps);
+ }
+
+ public static Path path(List steps) {
+ return new Path(steps);
+ }
+
+ public static Path path(String... steps) {
+ return new Path(steps);
+ }
+
+ public static Path path(Path previousSteps, String newStep) {
+ return new Path(previousSteps, newStep);
+ }
+
+ public List getSteps() {
+ return this.steps;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((this.steps == null) ? 0 : this.steps.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Path other = (Path) obj;
+ if (this.steps == null) {
+ if (other.steps != null) {
+ return false;
+ }
+ }
+ else if (!this.steps.equals(other.steps)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return new ToStringCreator(this).append("steps", this.steps).toString();
+ }
+
+ @Override
+ public int compareTo(Path o) {
+ int comparison = 0;
+ int size = min(this.steps.size(), o.getSteps().size());
+ for (int i = 0; i < size; i++) {
+ String thisStep = this.steps.get(i);
+ String thatStep = o.getSteps().get(i);
+ comparison = thisStep.compareTo(thatStep);
+ if (comparison != 0) {
+ break;
+ }
+ }
+ if (comparison == 0) {
+ comparison = ((Integer) this.steps.size()).compareTo(o.getSteps().size());
+ }
+ return comparison;
+ }
+}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentation.java
new file mode 100644
index 00000000..79777587
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentation.java
@@ -0,0 +1,94 @@
+/*
+ * 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.state;
+
+import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.REQUEST;
+import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.RESPONSE;
+import static org.springframework.restdocs.state.Path.path;
+
+import java.util.Arrays;
+
+import org.springframework.restdocs.RestDocumentationResultHandler;
+
+/**
+ * Static factory methods for documenting a RESTful API's state.
+ *
+ * @author Andreas Evers
+ */
+public abstract class StateDocumentation {
+
+ private StateDocumentation() {
+
+ }
+
+ /**
+ * Creates a {@code FieldDescriptor} that describes a field with the given
+ * {@code path}.
+ *
+ * @param path The path of the field
+ * @return a {@code FieldDescriptor} ready for further configuration
+ * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
+ * @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
+ */
+ public static FieldDescriptor fieldWithPath(Path path) {
+ return new FieldDescriptor(path);
+ }
+
+ /**
+ * Creates a {@code FieldDescriptor} that describes a field with the given
+ * {@code path}, in case the field is at the root of the request or response body
+ *
+ * @param name The name of the field being at the root of the request or response body
+ * @return a {@code FieldDescriptor} ready for further configuration
+ * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
+ * @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
+ */
+ public static FieldDescriptor fieldWithPath(String name) {
+ return new FieldDescriptor(path(name));
+ }
+
+ /**
+ * Creates a {@code RequestFieldsSnippetResultHandler} that will produce a
+ * documentation snippet for a request's fields.
+ *
+ * @param outputDir The directory to which the snippet should be written
+ * @param descriptors The descriptions of the request's fields
+ * @return the handler
+ * @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
+ */
+ public static FieldSnippetResultHandler documentRequestFields(String outputDir,
+ FieldDescriptor... descriptors) {
+ return new FieldSnippetResultHandler(outputDir, REQUEST,
+ Arrays.asList(descriptors));
+ }
+
+ /**
+ * Creates a {@code ResponseFieldsSnippetResultHandler} that will produce a
+ * documentation snippet for a response's fields.
+ *
+ * @param outputDir The directory to which the snippet should be written
+ * @param descriptors The descriptions of the response's fields
+ * @return the handler
+ * @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
+ */
+ public static FieldSnippetResultHandler documentResponseFields(String outputDir,
+ FieldDescriptor... descriptors) {
+ return new FieldSnippetResultHandler(outputDir, RESPONSE,
+ Arrays.asList(descriptors));
+ }
+
+}
diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentationValidator.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentationValidator.java
new file mode 100644
index 00000000..60409cbf
--- /dev/null
+++ b/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentationValidator.java
@@ -0,0 +1,75 @@
+/*
+ * 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.state;
+
+import static org.junit.Assert.fail;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.SortedSet;
+
+import org.springframework.restdocs.state.FieldSnippetResultHandler.Type;
+
+/**
+ * Validator which verifies if fields are documented correctly. All fields need to be
+ * documented except nested fields. For those fields it is sufficient that only the parent
+ * is documented. In case there are fields documented that don't appear in the actual
+ * request or response, the validation will fail.
+ *
+ * @author Andreas Evers
+ */
+public class StateDocumentationValidator {
+
+ private final Type type;
+
+ public StateDocumentationValidator(Type type) {
+ this.type = type;
+ }
+
+ public void validateFields(SortedSet actualFields,
+ SortedSet expectedFields) {
+ Set undocumentedFields = new HashSet(actualFields);
+ Set ignoredFields = new HashSet();
+ undocumentedFields.removeAll(expectedFields);
+ for (Path path : undocumentedFields) {
+ if (path.getSteps().size() > 1) {
+ Path wrappingPath = new Path(path);
+ wrappingPath.getSteps().remove(wrappingPath.getSteps().size() - 1);
+ if (actualFields.contains(wrappingPath)) {
+ ignoredFields.add(path);
+ }
+ }
+ }
+ undocumentedFields.removeAll(ignoredFields);
+
+ Set missingFields = new HashSet(expectedFields);
+ missingFields.removeAll(actualFields);
+
+ if (!undocumentedFields.isEmpty() || !missingFields.isEmpty()) {
+ String message = "";
+ if (!undocumentedFields.isEmpty()) {
+ message += "Fields with the following paths were not documented: "
+ + undocumentedFields;
+ }
+ if (!missingFields.isEmpty()) {
+ message += "Fields with the following paths were not found in the "
+ + this.type.toString().toLowerCase() + ": " + missingFields;
+ }
+ fail(message);
+ }
+ }
+}
diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java
new file mode 100644
index 00000000..a8533bbe
--- /dev/null
+++ b/spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java
@@ -0,0 +1,139 @@
+/*
+ * 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.state;
+
+import static org.junit.Assert.assertEquals;
+import static org.springframework.restdocs.state.Path.path;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.util.FileCopyUtils;
+
+/**
+ * Tests for {@link FieldExtractor}.
+ *
+ * @author Andreas Evers
+ */
+public class FieldExtractorTests {
+
+ private final FieldExtractor fieldExtractor = new FieldExtractor();
+
+ @Test
+ public void singleField() throws IOException {
+ Map fields = this.fieldExtractor
+ .extractFields(createResponse("single-field"));
+ assertFields(Arrays.asList(new Field(path("alpha"), "alpha-value")), fields);
+ }
+
+ @Test
+ public void multipleFields() throws IOException {
+ Map fields = this.fieldExtractor
+ .extractFields(createResponse("multiple-fields"));
+ assertFields(Arrays.asList(new Field(path("alpha"), "alpha-value"), new Field(
+ path("bravo"), 123), new Field(path("charlie"), createMap()), new Field(
+ path("delta"), createList()), new Field(path("echo"),
+ createListWithMaps())), fields);
+
+ }
+
+ private Map createMap() {
+ Map hashMap = new HashMap<>();
+ hashMap.put("one", 456);
+ hashMap.put("two", "two-value");
+ return hashMap;
+ }
+
+ private List createList() {
+ List arrayList = new ArrayList<>();
+ arrayList.add("delta-value-1");
+ arrayList.add("delta-value-2");
+ return arrayList;
+ }
+
+ private List