Allow an item to be undocumented without it causing a test failure

Previously, it was necessary to document every payload field, link,
path parameter, or request. If an item was not documented a failure
would occur. This has proven to be too restrictive for some use cases,
for example splitting up the documentation of a payload’s fields. While
it was possible to use a preprocessor to modify the operation prior to
documentation to remove the items that should not be documented, this
was more difficult than it needed to be.

This commit adds support for marking a descriptor as ignored. Ignored
descriptors count when checking that everything has been documented but
do not actually appear in the generated documentation.

Closes gh-143
This commit is contained in:
Andy Wilkinson
2015-09-30 17:33:29 +01:00
parent 7919845e8a
commit fb01a26a16
16 changed files with 226 additions and 15 deletions

View File

@@ -51,6 +51,10 @@ public abstract class HypermediaDocumentation {
* descriptors, a failure will occur when the snippet is invoked. Similarly, if a link
* is documented, is not marked as optional, and is not present in the response, a
* failure will also occur.
* <p>
* If you do not want to document a link, a link descriptor can be marked as
* {@link LinkDescriptor#ignored}. This will prevent it from appearing in the
* generated snippet while avoiding the failure described above.
*
* @param descriptors the descriptions of the response's links
* @return the snippet that will document the links
@@ -70,6 +74,10 @@ public abstract class HypermediaDocumentation {
* descriptors, a failure will occur when the snippet is invoked. Similarly, if a link
* is documented, is not marked as optional, and is not present in the response, a
* failure will also occur.
* <p>
* If you do not want to document a link, a link descriptor can be marked as
* {@link LinkDescriptor#ignored}. This will prevent it from appearing in the
* generated snippet while avoiding the failure described above.
*
* @param attributes the attributes
* @param descriptors the descriptions of the response's links
@@ -90,6 +98,10 @@ public abstract class HypermediaDocumentation {
* descriptors, a failure will occur when the snippet is invoked. Similarly, if a link
* is documented, is not marked as optional, and is not present in the response, a
* failure will also occur.
* <p>
* If you do not want to document a link, a link descriptor can be marked as
* {@link LinkDescriptor#ignored}. This will prevent it from appearing in the
* generated snippet while avoiding the failure described above.
*
* @param linkExtractor used to extract the links from the response
* @param descriptors the descriptions of the response's links
@@ -110,6 +122,10 @@ public abstract class HypermediaDocumentation {
* descriptors, a failure will occur when the snippet is invoked. Similarly, if a link
* is documented, is not marked as optional, and is not present in the response, a
* failure will also occur.
* <p>
* If you do not want to document a link, a link descriptor can be marked as
* {@link LinkDescriptor#ignored}. This will prevent it from appearing in the
* generated snippet while avoiding the failure described above.
*
* @param attributes the attributes
* @param linkExtractor used to extract the links from the response

View File

@@ -16,7 +16,7 @@
package org.springframework.restdocs.hypermedia;
import org.springframework.restdocs.snippet.AbstractDescriptor;
import org.springframework.restdocs.snippet.IgnorableDescriptor;
/**
* A description of a link found in a hypermedia API.
@@ -24,7 +24,7 @@ import org.springframework.restdocs.snippet.AbstractDescriptor;
* @author Andy Wilkinson
* @see HypermediaDocumentation#linkWithRel(String)
*/
public class LinkDescriptor extends AbstractDescriptor<LinkDescriptor> {
public class LinkDescriptor extends IgnorableDescriptor<LinkDescriptor> {
private final String rel;

View File

@@ -74,8 +74,12 @@ public class LinksSnippet extends TemplatedSnippet {
super("links", attributes);
this.linkExtractor = linkExtractor;
for (LinkDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getRel());
Assert.notNull(descriptor.getDescription());
Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel");
if (!descriptor.isIgnored()) {
Assert.notNull(descriptor.getDescription(), "The descriptor for link '"
+ descriptor.getRel() + "' must either have a description or be"
+ " marked as " + "ignored");
}
this.descriptorsByRel.put(descriptor.getRel(), descriptor);
}
}
@@ -131,7 +135,10 @@ public class LinksSnippet extends TemplatedSnippet {
private List<Map<String, Object>> createLinksModel() {
List<Map<String, Object>> model = new ArrayList<>();
for (Entry<String, LinkDescriptor> entry : this.descriptorsByRel.entrySet()) {
model.add(createModelForDescriptor(entry.getValue()));
LinkDescriptor descriptor = entry.getValue();
if (!descriptor.isIgnored()) {
model.add(createModelForDescriptor(descriptor));
}
}
return model;
}

View File

@@ -55,8 +55,13 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
Map<String, Object> attributes) {
super(type + "-fields", attributes);
for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath());
Assert.notNull(descriptor.getDescription());
Assert.notNull(descriptor.getPath(), "Field descriptors must have a path");
if (!descriptor.isIgnored()) {
Assert.notNull(descriptor.getDescription(), "The descriptor for field '"
+ descriptor.getPath() + "' must either have a description or"
+ " be marked as " + "ignored");
}
}
this.fieldDescriptors = descriptors;
}
@@ -77,7 +82,9 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
List<Map<String, Object>> fields = new ArrayList<>();
model.put("fields", fields);
for (FieldDescriptor descriptor : this.fieldDescriptors) {
fields.add(createModelForDescriptor(descriptor));
if (!descriptor.isIgnored()) {
fields.add(createModelForDescriptor(descriptor));
}
}
return model;
}

View File

@@ -16,7 +16,7 @@
package org.springframework.restdocs.payload;
import org.springframework.restdocs.snippet.AbstractDescriptor;
import org.springframework.restdocs.snippet.IgnorableDescriptor;
/**
* A description of a field found in a request or response payload.
@@ -25,7 +25,7 @@ import org.springframework.restdocs.snippet.AbstractDescriptor;
* @author Andy Wilkinson
* @see PayloadDocumentation#fieldWithPath(String)
*/
public class FieldDescriptor extends AbstractDescriptor<FieldDescriptor> {
public class FieldDescriptor extends IgnorableDescriptor<FieldDescriptor> {
private final String path;

View File

@@ -108,6 +108,10 @@ public abstract class PayloadDocumentation {
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
* <p>
* If you do not want to document a field, a field descriptor can be marked as
* {@link FieldDescriptor#ignored}. This will prevent it from appearing in the
* generated snippet while avoiding the failure described above.
*
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
@@ -128,6 +132,10 @@ public abstract class PayloadDocumentation {
* payload, a failure will also occur. For payloads with a hierarchical structure,
* documenting a field is sufficient for all of its descendants to also be treated as
* having been documented.
* <p>
* If you do not want to document a field, a field descriptor can be marked as
* {@link FieldDescriptor#ignored}. This will prevent it from appearing in the
* generated snippet while avoiding the failure described above.
*
* @param attributes the attributes
* @param descriptors the descriptions of the request payload's fields
@@ -150,6 +158,10 @@ public abstract class PayloadDocumentation {
* payload, a failure will also occur. For payloads with a hierarchical structure,
* documenting a field is sufficient for all of its descendants to also be treated as
* having been documented.
* <p>
* If you do not want to document a field, a field descriptor can be marked as
* {@link FieldDescriptor#ignored}. This will prevent it from appearing in the
* generated snippet while avoiding the failure described above.
*
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
@@ -170,6 +182,10 @@ public abstract class PayloadDocumentation {
* payload, a failure will also occur. For payloads with a hierarchical structure,
* documenting a field is sufficient for all of its descendants to also be treated as
* having been documented.
* <p>
* If you do not want to document a field, a field descriptor can be marked as
* {@link FieldDescriptor#ignored}. This will prevent it from appearing in the
* generated snippet while avoiding the failure described above.
*
* @param attributes the attributes
* @param descriptors the descriptions of the response payload's fields

View File

@@ -54,8 +54,13 @@ public abstract class AbstractParametersSnippet extends TemplatedSnippet {
List<ParameterDescriptor> descriptors, Map<String, Object> attributes) {
super(snippetName, attributes);
for (ParameterDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getName());
Assert.notNull(descriptor.getDescription());
Assert.notNull(descriptor.getName(), "Parameter descriptors must have a name");
if (!descriptor.isIgnored()) {
Assert.notNull(descriptor.getDescription(),
"The descriptor for parameter '" + descriptor.getName()
+ "' must either have a description or be marked as "
+ "ignored");
}
this.descriptorsByName.put(descriptor.getName(), descriptor);
}
}
@@ -67,7 +72,10 @@ public abstract class AbstractParametersSnippet extends TemplatedSnippet {
Map<String, Object> model = new HashMap<>();
List<Map<String, Object>> parameters = new ArrayList<>();
for (Entry<String, ParameterDescriptor> entry : this.descriptorsByName.entrySet()) {
parameters.add(createModelForDescriptor(entry.getValue()));
ParameterDescriptor descriptor = entry.getValue();
if (!descriptor.isIgnored()) {
parameters.add(createModelForDescriptor(descriptor));
}
}
model.put("parameters", parameters);
return model;

View File

@@ -16,7 +16,7 @@
package org.springframework.restdocs.request;
import org.springframework.restdocs.snippet.AbstractDescriptor;
import org.springframework.restdocs.snippet.IgnorableDescriptor;
/**
* A descriptor of a request or path parameter.
@@ -25,7 +25,7 @@ import org.springframework.restdocs.snippet.AbstractDescriptor;
* @see RequestDocumentation#parameterWithName
*
*/
public class ParameterDescriptor extends AbstractDescriptor<ParameterDescriptor> {
public class ParameterDescriptor extends IgnorableDescriptor<ParameterDescriptor> {
private final String name;

View File

@@ -53,6 +53,10 @@ public abstract class RequestDocumentation {
* descriptors, a failure will occur when the snippet is invoked. Similarly, if a
* parameter is documented, is not marked as optional, and is not present in the
* request path, a failure will also occur.
* <p>
* If you do not want to document a path parameter, a parameter descriptor can be
* marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing
* in the generated snippet while avoiding the failure described above.
*
* @param descriptors the descriptions of the parameters in the request's path
* @return the snippet that will document the parameters
@@ -71,6 +75,10 @@ public abstract class RequestDocumentation {
* descriptors, a failure will occur when the snippet is invoked. Similarly, if a
* parameter is documented, is not marked as optional, and is not present in the
* request path, a failure will also occur.
* <p>
* If you do not want to document a path parameter, a parameter descriptor can be
* marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing
* in the generated snippet while avoiding the failure described above.
*
* @param attributes the attributes
* @param descriptors the descriptions of the parameters in the request's path
@@ -90,6 +98,10 @@ public abstract class RequestDocumentation {
* descriptors, a failure will occur when the snippet is invoked. Similarly, if a
* parameter is documented, is not marked as optional, and is not present in the
* request, a failure will also occur.
* <p>
* If you do not want to document a request parameter, a parameter descriptor can be
* marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing
* in the generated snippet while avoiding the failure described above.
*
* @param descriptors The descriptions of the request's parameters
* @return the snippet
@@ -109,6 +121,10 @@ public abstract class RequestDocumentation {
* descriptors, a failure will occur when the snippet is invoked. Similarly, if a
* parameter is documented, is not marked as optional, and is not present in the
* request, a failure will also occur.
* <p>
* If you do not want to document a request parameter, a parameter descriptor can be
* marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing
* in the generated snippet while avoiding the failure described above.
*
* @param attributes the attributes
* @param descriptors the descriptions of the request's parameters

View File

@@ -0,0 +1,52 @@
/*
* 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.snippet;
/**
* Base class for descriptors for items that can be ignored.
*
* @param <T> the type of the descriptor
* @author Andy Wilkinson
*/
public abstract class IgnorableDescriptor<T extends IgnorableDescriptor<T>> extends
AbstractDescriptor<T> {
private boolean ignored = false;
/**
* Marks the described item as being ignored. Ignored items are not included in the
* generated documentation.
*
* @return the descriptor
*/
@SuppressWarnings("unchecked")
public final T ignored() {
this.ignored = true;
return (T) this;
}
/**
* Returns whether or not the item being described should be ignored and, therefore,
* should not be included in the documentation.
*
* @return {@code true} if the item should be ignored, otherwise {@code false}.
*/
public final boolean isIgnored() {
return this.ignored;
}
}

View File

@@ -65,6 +65,17 @@ public class LinksSnippetTests {
"undocumented-link", this.snippet.getOutputDirectory()).build());
}
@Test
public void ignoredLink() throws IOException {
this.snippet.expectLinks("ignored-link").withContents(
tableWithHeader("Relation", "Description").row("b", "Link b"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"),
new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(),
new LinkDescriptor("b").description("Link b")))
.document(new OperationBuilder("ignored-link", this.snippet
.getOutputDirectory()).build());
}
@Test
public void missingLink() throws IOException {
this.thrown.expect(SnippetException.class);

View File

@@ -98,6 +98,19 @@ public class RequestFieldsSnippetTests {
.content("{\"a\": 5}").build());
}
@Test
public void ignoredRequestField() throws IOException {
this.snippet.expectRequestFields("ignored-request-field").withContents(
tableWithHeader("Path", "Type", "Description").row("b", "Number",
"Field b"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(),
fieldWithPath("b").description("Field b")))
.document(new OperationBuilder("ignored-request-field", this.snippet
.getOutputDirectory()).request("http://localhost")
.content("{\"a\": 5, \"b\": 4}").build());
}
@Test
public void missingRequestField() throws IOException {
this.thrown.expect(SnippetException.class);

View File

@@ -106,6 +106,19 @@ public class ResponseFieldsSnippetTests {
.content("[\"a\", \"b\", \"c\"]").build());
}
@Test
public void ignoredResponseField() throws IOException {
this.snippet.expectResponseFields("ignored-response-field").withContents(
tableWithHeader("Path", "Type", "Description").row("b", "Number",
"Field b"));
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(),
fieldWithPath("b").description("Field b")))
.document(new OperationBuilder("ignored-response-field", this.snippet
.getOutputDirectory()).response().content("{\"a\": 5, \"b\": 4}")
.build());
}
@Test
public void responseFieldsWithCustomDescriptorAttributes() throws IOException {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);

View File

@@ -104,6 +104,18 @@ public class PathParametersSnippetTests {
"org.springframework.restdocs.urlTemplate", "/{a}/{b}").build());
}
@Test
public void ignoredPathParameter() throws IOException {
this.snippet.expectPathParameters("ignored-path-parameter").withContents(
tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b",
"two"));
new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(),
parameterWithName("b").description("two")))
.document(new OperationBuilder("ignored-path-parameter", this.snippet
.getOutputDirectory()).attribute(
"org.springframework.restdocs.urlTemplate", "/{a}/{b}").build());
}
@Test
public void pathParametersWithQueryString() throws IOException {
this.snippet.expectPathParameters("path-parameters-with-query-string")

View File

@@ -102,6 +102,17 @@ public class RequestParametersSnippetTests {
.build());
}
@Test
public void ignoredRequestParameter() throws IOException {
this.snippet.expectRequestParameters("ignored-request-parameter").withContents(
tableWithHeader("Parameter", "Description").row("b", "two"));
new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(),
parameterWithName("b").description("two")))
.document(new OperationBuilder("ignored-request-parameter", this.snippet
.getOutputDirectory()).request("http://localhost")
.param("a", "bravo").param("b", "bravo").build());
}
@Test
public void requestParametersWithCustomDescriptorAttributes() throws IOException {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);

View File

@@ -400,6 +400,30 @@ public class MockMvcRestDocumentationIntegrationTests {
"$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'"))));
}
@Test
public void stackOverflowQuestion() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation)).build();
mockMvc.perform(get("/company/5").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document(
"company",
responseFields(
fieldWithPath("companyName").description(
"The name of the company"),
fieldWithPath("employee").description(
"An array of the company's employees"))))
.andDo(document(
"employee",
responseFields(
fieldWithPath("companyName").ignored(),
fieldWithPath("employee[].name").description(
"The name of the employee"),
fieldWithPath("employee[].age").description(
"The age of the employee"))));
}
private void assertExpectedSnippetFilesExist(File directory, String... snippets) {
for (String snippet : snippets) {
assertTrue(new File(directory, snippet).isFile());
@@ -434,6 +458,11 @@ public class MockMvcRestDocumentationIntegrationTests {
HttpStatus.OK);
}
@RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE)
public String bar() {
return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}";
}
}
}