Initial support for documenting fields in requests and responses
This commit introduces support for documenting the fields in a request and response, modelled on the existing support for documenting links. A test with field documentation enabled will fail if a documented field is missing from the request or response, or if a field present in the request or response has not been documented. See gh-24 Closes gh-25
This commit is contained in:
committed by
Andy Wilkinson
parent
cc54e5d7b0
commit
ecfc7abd92
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -59,5 +59,4 @@ public abstract class HypermediaDocumentation {
|
||||
return new LinkSnippetResultHandler(outputDir, linkExtractor,
|
||||
Arrays.asList(descriptors));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Path, Field> extractedFields = new HashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<Path, Field> extractFields(MockHttpServletRequest request)
|
||||
throws IOException {
|
||||
Map<String, Object> jsonContent = this.objectMapper.readValue(
|
||||
request.getInputStream(), Map.class);
|
||||
extractFieldsRecursively(jsonContent);
|
||||
return this.extractedFields;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<Path, Field> extractFields(MockHttpServletResponse response)
|
||||
throws IOException {
|
||||
String responseBody = response.getContentAsString();
|
||||
Assert.hasText(responseBody,
|
||||
"The response doesn't contain a body to extract fields from");
|
||||
Map<String, Object> jsonContent = this.objectMapper.readValue(responseBody,
|
||||
Map.class);
|
||||
extractFieldsRecursively(jsonContent);
|
||||
return this.extractedFields;
|
||||
}
|
||||
|
||||
private void extractFieldsRecursively(Map<String, Object> jsonContent) {
|
||||
extractFieldsRecursively(null, jsonContent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void extractFieldsRecursively(Path previousSteps,
|
||||
Map<String, Object> jsonContent) {
|
||||
for (Entry<String, Object> 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<String, Object> value = (Map<String, Object>) entry.getValue();
|
||||
extractFieldsRecursively(path, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Path, FieldDescriptor> descriptorsByName = new HashMap<Path, FieldDescriptor>();
|
||||
|
||||
private final Type type;
|
||||
|
||||
private FieldExtractor extractor = new FieldExtractor();
|
||||
|
||||
private StateDocumentationValidator validator;
|
||||
|
||||
enum Type {
|
||||
REQUEST, RESPONSE;
|
||||
}
|
||||
|
||||
FieldSnippetResultHandler(String outputDir, FieldSnippetResultHandler.Type type,
|
||||
List<FieldDescriptor> 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<Path, Field> fields;
|
||||
if (this.type == REQUEST) {
|
||||
fields = this.extractor.extractFields(result.getRequest());
|
||||
}
|
||||
else {
|
||||
fields = this.extractor.extractFields(result.getResponse());
|
||||
}
|
||||
|
||||
SortedSet<Path> actualFields = new TreeSet<Path>(fields.keySet());
|
||||
SortedSet<Path> expectedFields = new TreeSet<Path>(
|
||||
this.descriptorsByName.keySet());
|
||||
|
||||
this.validator.validateFields(actualFields, expectedFields);
|
||||
|
||||
writer.println("|===");
|
||||
writer.println("| Path | Description | Type | Required | Constraints");
|
||||
|
||||
for (Entry<Path, FieldDescriptor> 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("|===");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Path> {
|
||||
|
||||
private List<String> steps = new ArrayList<>();
|
||||
|
||||
public Path(Path path) {
|
||||
this.steps = new ArrayList<String>(path.getSteps());
|
||||
}
|
||||
|
||||
public Path(List<String> 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<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Path> actualFields,
|
||||
SortedSet<Path> expectedFields) {
|
||||
Set<Path> undocumentedFields = new HashSet<Path>(actualFields);
|
||||
Set<Path> ignoredFields = new HashSet<Path>();
|
||||
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<Path> missingFields = new HashSet<Path>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("single-field"));
|
||||
assertFields(Arrays.asList(new Field(path("alpha"), "alpha-value")), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFields() throws IOException {
|
||||
Map<Path, Field> 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<String, Object> createMap() {
|
||||
Map<String, Object> hashMap = new HashMap<>();
|
||||
hashMap.put("one", 456);
|
||||
hashMap.put("two", "two-value");
|
||||
return hashMap;
|
||||
}
|
||||
|
||||
private List<String> createList() {
|
||||
List<String> arrayList = new ArrayList<>();
|
||||
arrayList.add("delta-value-1");
|
||||
arrayList.add("delta-value-2");
|
||||
return arrayList;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> createListWithMaps() {
|
||||
List<Map<String, Object>> arrayList = new ArrayList<>();
|
||||
Map<String, Object> hashMap1 = new HashMap<>();
|
||||
hashMap1.put("one", 789);
|
||||
hashMap1.put("two", "two-value");
|
||||
arrayList.add(hashMap1);
|
||||
Map<String, Object> hashMap2 = new HashMap<>();
|
||||
hashMap2.put("one", 987);
|
||||
hashMap2.put("two", "value-two");
|
||||
arrayList.add(hashMap2);
|
||||
return arrayList;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFieldsAndLinks() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("multiple-fields-and-links"));
|
||||
assertFields(Arrays.asList(new Field(path("beta"), "beta-value"), new Field(
|
||||
path("charlie"), "charlie-value")), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFieldsAndEmbedded() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("multiple-fields-and-embedded"));
|
||||
assertFields(Arrays.asList(new Field(path("beta"), "beta-value"), new Field(
|
||||
path("charlie"), "charlie-value")), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFieldsAndEmbeddedAndLinks() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("multiple-fields-and-embedded-and-links"));
|
||||
assertFields(Arrays.asList(new Field(path("beta"), "beta-value"), new Field(
|
||||
path("charlie"), "charlie-value")), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noFields() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("no-fields"));
|
||||
assertFields(Collections.<Field> emptyList(), fields);
|
||||
}
|
||||
|
||||
private void assertFields(List<Field> expectedFields, Map<Path, Field> actualFields) {
|
||||
Map<Path, Field> expectedFieldsByName = new HashMap<>();
|
||||
for (Field expectedField : expectedFields) {
|
||||
expectedFieldsByName.put(expectedField.getPath(), expectedField);
|
||||
}
|
||||
assertEquals(expectedFieldsByName, actualFields);
|
||||
}
|
||||
|
||||
private MockHttpServletResponse createResponse(String contentName) throws IOException {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FileCopyUtils.copy(new FileReader(getPayloadFile(contentName)),
|
||||
response.getWriter());
|
||||
return response;
|
||||
}
|
||||
|
||||
private File getPayloadFile(String name) {
|
||||
return new File("src/test/resources/field-payloads/" + name + ".json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package org.springframework.restdocs.state;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.REQUEST;
|
||||
import static org.springframework.restdocs.state.Path.path;
|
||||
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
public class StateDocumentationValidatorTests {
|
||||
|
||||
SortedSet<Path> actualFields = new TreeSet<Path>();
|
||||
|
||||
SortedSet<Path> expectedFields = new TreeSet<Path>();
|
||||
|
||||
StateDocumentationValidator validator = new StateDocumentationValidator(REQUEST);
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
this.actualFields = new TreeSet<Path>();
|
||||
this.expectedFields = new TreeSet<Path>();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalFields() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void sameLevelButMoreDocumented() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void sameLevelButMoreActuals() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void moreDocumentedButParentPresent() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moreActualsButParentPresent() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("charlie"), path("charlie", "marco")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void documentationSkippedLevel() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco", "alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void moreActualsWithoutParentPresent() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"_links": {
|
||||
"alpha": "http://alpha.example.com",
|
||||
"bravo": "http://bravo.example.com"
|
||||
},
|
||||
"_embedded": "embedded-test",
|
||||
"beta": "beta-value",
|
||||
"charlie": "charlie-value"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"_embedded": "embedded-test",
|
||||
"beta": "beta-value",
|
||||
"charlie": "charlie-value"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"_links": {
|
||||
"alpha": "http://alpha.example.com",
|
||||
"bravo": "http://bravo.example.com"
|
||||
},
|
||||
"beta": "beta-value",
|
||||
"charlie": "charlie-value"
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"alpha": "alpha-value",
|
||||
"bravo": 123,
|
||||
"charlie": {
|
||||
"one": 456,
|
||||
"two": "two-value"
|
||||
},
|
||||
"delta": [
|
||||
"delta-value-1",
|
||||
"delta-value-2"
|
||||
],
|
||||
"echo": [{
|
||||
"one": 789,
|
||||
"two": "two-value"
|
||||
},{
|
||||
"one": 987,
|
||||
"two": "value-two"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ }
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"alpha": "alpha-value"
|
||||
}
|
||||
Reference in New Issue
Block a user