From 043e796502d7b283da15b6da46305976c7e671dc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Apr 2016 12:41:05 +0100 Subject: [PATCH] =?UTF-8?q?Introduce=20relaxed=20snippets=20that=20don?= =?UTF-8?q?=E2=80=99t=20fail=20when=20item=20is=20not=20documented?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, many of the snippets would fail when something wasn’t documented. This worked well when writing exhaustive API documentation, but was cumbersome when trying to document a scenario that might only being interested in a subset of the links, response fields, etc. It was necessary to mark things that were not of interest as being ignored. This commit introduces a relaxed variant of several snippets. A relaxed snippet will not fail if something has not been documented. Instead, the undocumented thing will be ignored. If something has been documented but it does not exist a failure will still occur. Closes gh-175 --- .../hypermedia/HypermediaDocumentation.java | 100 ++++++++++++++++++ .../restdocs/hypermedia/LinksSnippet.java | 50 ++++++++- .../payload/AbstractFieldsSnippet.java | 32 +++++- .../payload/PayloadDocumentation.java | 69 ++++++++++++ .../payload/RequestFieldsSnippet.java | 39 ++++++- .../payload/ResponseFieldsSnippet.java | 40 ++++++- .../request/AbstractParametersSnippet.java | 38 ++++++- .../request/PathParametersSnippet.java | 42 +++++++- .../request/RequestDocumentation.java | 72 ++++++++++++- .../request/RequestParametersSnippet.java | 45 +++++++- .../hypermedia/LinksSnippetTests.java | 11 ++ .../payload/RequestFieldsSnippetTests.java | 13 +++ .../payload/ResponseFieldsSnippetTests.java | 12 +++ .../request/PathParametersSnippetTests.java | 13 +++ .../RequestParametersSnippetTests.java | 11 ++ 15 files changed, 559 insertions(+), 28 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index f5493128..95f70b3b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -66,6 +66,26 @@ public abstract class HypermediaDocumentation { Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response automatically based on its + * content type and will be documented using the given {@code descriptors}. + *

+ * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

+ * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkDescriptor... descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), + Arrays.asList(descriptors), true); + } + /** * Returns a new {@code Snippet} that will document the links in the API call's * response. The given {@code attributes} will be available during snippet generation. @@ -80,6 +100,10 @@ public abstract class HypermediaDocumentation { * 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. + *

+ * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param attributes the attributes * @param descriptors the descriptions of the response's links @@ -91,6 +115,29 @@ public abstract class HypermediaDocumentation { Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document the links in the API call's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response automatically based on its content type + * and will be documented using the given {@code descriptors}. + *

+ * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

+ * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(Map attributes, + LinkDescriptor... descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), + Arrays.asList(descriptors), attributes, true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. Links will be extracted from the response using the given @@ -104,6 +151,10 @@ public abstract class HypermediaDocumentation { * 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. + *

+ * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links @@ -114,6 +165,27 @@ public abstract class HypermediaDocumentation { return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response using the given + * {@code linkExtractor} and will be documented using the given {@code descriptors}. + *

+ * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

+ * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + LinkDescriptor... descriptors) { + return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. The given {@code attributes} will be available during snippet generation. @@ -128,6 +200,10 @@ public abstract class HypermediaDocumentation { * 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. + *

+ * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param attributes the attributes * @param linkExtractor used to extract the links from the response @@ -139,6 +215,30 @@ public abstract class HypermediaDocumentation { return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response using the given {@code linkExtractor} and + * will be documented using the given {@code descriptors}. + *

+ * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

+ * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + Map attributes, LinkDescriptor... descriptors) { + return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes, + true); + } + /** * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia * Application Language (HAL) format where the links are found in a map named diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 528b444a..51b28f92 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -19,6 +19,7 @@ package org.springframework.restdocs.hypermedia; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -50,22 +51,41 @@ public class LinksSnippet extends TemplatedSnippet { private final LinkExtractor linkExtractor; + private final boolean ignoreUndocumentedLinks; + /** * Creates a new {@code LinksSnippet} that will extract links using the given * {@code linkExtractor} and document them using the given {@code descriptors}. + * Undocumented links will trigger a failure. * * @param linkExtractor the link extractor * @param descriptors the link descriptors */ protected LinksSnippet(LinkExtractor linkExtractor, List descriptors) { - this(linkExtractor, descriptors, null); + this(linkExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code LinksSnippet} that will extract links using the given + * {@code linkExtractor} and document them using the given {@code descriptors}. If + * {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be ignored + * and will not trigger a failure. + * + * @param linkExtractor the link extractor + * @param descriptors the link descriptors + * @param ignoreUndocumentedLinks whether undocumented links should be ignored + */ + protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, + boolean ignoreUndocumentedLinks) { + this(linkExtractor, descriptors, null, ignoreUndocumentedLinks); } /** * Creates a new {@code LinksSnippet} that will extract links using the given * {@code linkExtractor} and document them using the given {@code descriptors}. The * given {@code attributes} will be included in the model during template rendering. + * Undocumented links will trigger a failure. * * @param linkExtractor the link extractor * @param descriptors the link descriptors @@ -73,12 +93,30 @@ public class LinksSnippet extends TemplatedSnippet { */ protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, Map attributes) { + this(linkExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code LinksSnippet} that will extract links using the given + * {@code linkExtractor} and document them using the given {@code descriptors}. The + * given {@code attributes} will be included in the model during template rendering. + * If {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be + * ignored and will not trigger a failure. + * + * @param linkExtractor the link extractor + * @param descriptors the link descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedLinks whether undocumented links should be ignored + */ + protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, + Map attributes, boolean ignoreUndocumentedLinks) { super("links", attributes); this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel"); this.descriptorsByRel.put(descriptor.getRel(), descriptor); } + this.ignoreUndocumentedLinks = ignoreUndocumentedLinks; } @Override @@ -100,8 +138,14 @@ public class LinksSnippet extends TemplatedSnippet { private void validate(Map> links) { Set actualRels = links.keySet(); - Set undocumentedRels = new HashSet<>(actualRels); - undocumentedRels.removeAll(this.descriptorsByRel.keySet()); + Set undocumentedRels; + if (this.ignoreUndocumentedLinks) { + undocumentedRels = Collections.emptySet(); + } + else { + undocumentedRels = new HashSet<>(actualRels); + undocumentedRels.removeAll(this.descriptorsByRel.keySet()); + } Set requiredRels = new HashSet<>(); for (Entry relAndDescriptor : this.descriptorsByRel diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 19322b28..c6450712 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -39,20 +39,42 @@ import org.springframework.util.StringUtils; */ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { - private List fieldDescriptors; + private final List fieldDescriptors; + + private final boolean ignoreUndocumentedFields; /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields}. The fields will be documented using the given * {@code descriptors} and the given {@code attributes} will be included in the model - * during template rendering. + * during template rendering. Undocumented fields will trigger a failure. * * @param type the type of the fields * @param descriptors the field descriptors * @param attributes the additional attributes + * @deprecated since 1.1 in favor of + * {@link #AbstractFieldsSnippet(String, List, Map, boolean)} */ + @Deprecated protected AbstractFieldsSnippet(String type, List descriptors, Map attributes) { + this(type, descriptors, attributes, false); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be included in the model + * during template rendering. If {@code ignoreUndocumentedFields} is {@code true}, + * undocumented fields will be ignored and will not trigger a failure. + * + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected AbstractFieldsSnippet(String type, List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { super(type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); @@ -65,6 +87,7 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { } this.fieldDescriptors = descriptors; + this.ignoreUndocumentedFields = ignoreUndocumentedFields; } @Override @@ -111,8 +134,9 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private void validateFieldDocumentation(ContentHandler payloadHandler) { List missingFields = payloadHandler .findMissingFields(this.fieldDescriptors); - String undocumentedPayload = payloadHandler - .getUndocumentedContent(this.fieldDescriptors); + + String undocumentedPayload = this.ignoreUndocumentedFields ? null + : payloadHandler.getUndocumentedContent(this.fieldDescriptors); if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) { String message = ""; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index f2391277..bb94c960 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -119,6 +119,22 @@ public abstract class PayloadDocumentation { return new RequestFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operations's + * request payload. The fields will be documented using the given {@code descriptors}. + *

+ * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + FieldDescriptor... descriptors) { + return new RequestFieldsSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * request payload. The fields will be documented using the given {@code descriptors} @@ -145,6 +161,24 @@ public abstract class PayloadDocumentation { return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * request payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

+ * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + Map attributes, FieldDescriptor... descriptors) { + return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -169,6 +203,23 @@ public abstract class PayloadDocumentation { return new ResponseFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

+ * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + FieldDescriptor... descriptors) { + return new ResponseFieldsSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -195,4 +246,22 @@ public abstract class PayloadDocumentation { return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

+ * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + Map attributes, FieldDescriptor... descriptors) { + return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes, true); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 9e296887..cfb2ee26 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -37,25 +37,56 @@ public class RequestFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code RequestFieldsSnippet} that will document the fields in the - * request using the given {@code descriptors}. + * request using the given {@code descriptors}. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors */ protected RequestFieldsSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. If {@code ignoreUndocumentedFields} is + * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestFieldsSnippet(List descriptors, + boolean ignoreUndocumentedFields) { + this(descriptors, null, ignoreUndocumentedFields); } /** * Creates a new {@code RequestFieldsSnippet} that will document the fields in the * request using the given {@code descriptors}. The given {@code attributes} will be - * included in the model during template rendering. + * included in the model during template rendering. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors * @param attributes the additional attributes */ protected RequestFieldsSnippet(List descriptors, Map attributes) { - super("request", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestFieldsSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + super("request", descriptors, attributes, ignoreUndocumentedFields); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 28815c4c..06b5ac65 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -37,25 +37,57 @@ public class ResponseFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the - * response using the given {@code descriptors}. + * response using the given {@code descriptors}. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors */ protected ResponseFieldsSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * response using the given {@code descriptors}. If {@code ignoreUndocumentedFields} + * is {@code true}, undocumented fields will be ignored and will not trigger a + * failure. + * + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected ResponseFieldsSnippet(List descriptors, + boolean ignoreUndocumentedFields) { + this(descriptors, null, ignoreUndocumentedFields); } /** * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the * response using the given {@code descriptors}. The given {@code attributes} will be - * included in the model during template rendering. + * included in the model during template rendering. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors * @param attributes the additional attributes */ protected ResponseFieldsSnippet(List descriptors, Map attributes) { - super("response", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected ResponseFieldsSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + super("response", descriptors, attributes, ignoreUndocumentedFields); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index b132aa08..9d97737d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.request; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -40,18 +41,42 @@ public abstract class AbstractParametersSnippet extends TemplatedSnippet { private final Map descriptorsByName = new LinkedHashMap<>(); + private final boolean ignoreUndocumentedParameters; + /** * Creates a new {@code AbstractParametersSnippet} that will produce a snippet with * the given {@code snippetName} that will document parameters using the given * {@code descriptors}. The given {@code attributes} will be included in the model - * during template rendering. + * during template rendering. Undocumented parameters will trigger a failure. * * @param snippetName The snippet name * @param descriptors The descriptors * @param attributes The additional attributes + * @deprecated since 1.1 in favour of + * {@link #AbstractParametersSnippet(String, List, Map, boolean)} */ + @Deprecated protected AbstractParametersSnippet(String snippetName, List descriptors, Map attributes) { + this(snippetName, descriptors, attributes, false); + } + + /** + * Creates a new {@code AbstractParametersSnippet} that will produce a snippet with + * the given {@code snippetName} that will document parameters using the given + * {@code descriptors}. The given {@code attributes} will be included in the model + * during template rendering. If {@code ignoreUndocumentedParameters} is {@code true}, + * undocumented parameters will be ignored and will not trigger a failure. + * + * @param snippetName The snippet name + * @param descriptors The descriptors + * @param attributes The additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected AbstractParametersSnippet(String snippetName, + List descriptors, Map attributes, + boolean ignoreUndocumentedParameters) { super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getName(), @@ -64,6 +89,7 @@ public abstract class AbstractParametersSnippet extends TemplatedSnippet { } this.descriptorsByName.put(descriptor.getName(), descriptor); } + this.ignoreUndocumentedParameters = ignoreUndocumentedParameters; } @Override @@ -92,8 +118,14 @@ public abstract class AbstractParametersSnippet extends TemplatedSnippet { expectedParameters.add(entry.getKey()); } } - Set undocumentedParameters = new HashSet<>(actualParameters); - undocumentedParameters.removeAll(expectedParameters); + Set undocumentedParameters; + if (this.ignoreUndocumentedParameters) { + undocumentedParameters = Collections.emptySet(); + } + else { + undocumentedParameters = new HashSet<>(actualParameters); + undocumentedParameters.removeAll(expectedParameters); + } Set missingParameters = new HashSet<>(expectedParameters); missingParameters.removeAll(actualParameters); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 01ef25e4..509a604b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -44,25 +44,59 @@ public class PathParametersSnippet extends AbstractParametersSnippet { /** * Creates a new {@code PathParametersSnippet} that will document the request's path - * parameters using the given {@code descriptors}. + * parameters using the given {@code descriptors}. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors */ protected PathParametersSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code PathParametersSnippet} that will document the request's path + * parameters using the given {@code descriptors}. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected PathParametersSnippet(List descriptors, + boolean ignoreUndocumentedParameters) { + this(descriptors, null, ignoreUndocumentedParameters); } /** * Creates a new {@code PathParametersSnippet} that will document the request's path * parameters using the given {@code descriptors}. The given {@code attributes} will - * be included in the model during template rendering. + * be included in the model during template rendering. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ protected PathParametersSnippet(List descriptors, Map attributes) { - super("path-parameters", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code PathParametersSnippet} that will document the request's path + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected PathParametersSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedParameters) { + super("path-parameters", descriptors, attributes, ignoreUndocumentedParameters); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 6fa64c9b..4c4aee73 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -65,6 +65,22 @@ public abstract class RequestDocumentation { return new PathParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

+ * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + ParameterDescriptor... descriptors) { + return new PathParametersSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -89,6 +105,24 @@ public abstract class RequestDocumentation { return new PathParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

+ * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + Map attributes, ParameterDescriptor... descriptors) { + return new PathParametersSnippet(Arrays.asList(descriptors), attributes, true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The parameters will be documented using the given @@ -112,6 +146,23 @@ public abstract class RequestDocumentation { return new RequestParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

+ * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors The descriptions of the request's parameters + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + ParameterDescriptor... descriptors) { + return new RequestParametersSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -137,4 +188,23 @@ public abstract class RequestDocumentation { return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

+ * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parameters + * @return the snippet that will document the parameters + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + Map attributes, ParameterDescriptor... descriptors) { + return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index e85423fc..37a5696d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -42,25 +42,60 @@ public class RequestParametersSnippet extends AbstractParametersSnippet { /** * Creates a new {@code RequestParametersSnippet} that will document the request's - * parameters using the given {@code descriptors}. + * parameters using the given {@code descriptors}. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors */ protected RequestParametersSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestParametersSnippet} that will document the request's + * parameters using the given {@code descriptors}. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected RequestParametersSnippet(List descriptors, + boolean ignoreUndocumentedParameters) { + this(descriptors, null, ignoreUndocumentedParameters); } /** * Creates a new {@code RequestParametersSnippet} that will document the request's * parameters using the given {@code descriptors}. The given {@code attributes} will - * be included in the model during template rendering. + * be included in the model during template rendering. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ protected RequestParametersSnippet(List descriptors, Map attributes) { - super("request-parameters", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestParametersSnippet} that will document the request's + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected RequestParametersSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedParameters) { + super("request-parameters", descriptors, attributes, + ignoreUndocumentedParameters); } @Override diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index be122de6..e94abfc3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -56,6 +56,17 @@ public class LinksSnippetTests extends AbstractSnippetTests { .document(operationBuilder("ignored-link").build()); } + @Test + public void allUndocumentedLinksCanBeIgnored() throws IOException { + this.snippet.expectLinks("ignore-all-undocumented").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("b").description("Link b")), true) + .document(operationBuilder("ignore-all-undocumented").build()); + } + @Test public void documentedOptionalLink() throws IOException { this.snippet.expectLinks("documented-optional-link").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 79e02a8c..d94a9071 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -93,6 +93,19 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { + this.snippet.expectRequestFields("ignore-all-undocumented") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), + true).document( + operationBuilder("ignore-all-undocumented") + .request("http://localhost") + .content("{\"a\": 5, \"b\": 4}").build()); + } + @Test public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index ae490e50..215eb4c8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -105,6 +105,18 @@ public class ResponseFieldsSnippetTests extends AbstractSnippetTests { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void allUndocumentedFieldsCanBeIgnored() throws IOException { + this.snippet.expectResponseFields("ignore-all-undocumented") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); + + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("b").description("Field b")), true) + .document(operationBuilder("ignore-all-undocumented").response() + .content("{\"a\": 5, \"b\": 4}").build()); + } + @Test public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 05ca00c9..c3443048 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -71,6 +71,19 @@ public class PathParametersSnippetTests extends AbstractSnippetTests { "/{a}/{b}").build()); } + @Test + public void allUndocumentedPathParametersCanBeIgnored() throws IOException { + this.snippet.expectPathParameters("ignore-all-undocumented").withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("b", + "two")); + new PathParametersSnippet( + Arrays.asList(parameterWithName("b").description("two")), + true).document(operationBuilder("ignore-all-undocumented") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") + .build()); + } + @Test public void missingOptionalPathParameter() throws IOException { this.snippet diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 16baa187..a62c5de2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -80,6 +80,17 @@ public class RequestParametersSnippetTests extends AbstractSnippetTests { .param("b", "bravo").build()); } + @Test + public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { + this.snippet.expectRequestParameters("ignore-all-undocumented").withContents( + tableWithHeader("Parameter", "Description").row("b", "two")); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("b").description("two")), true) + .document(operationBuilder("ignore-all-undocumented") + .request("http://localhost").param("a", "bravo") + .param("b", "bravo").build()); + } + @Test public void missingOptionalRequestParameter() throws IOException { this.snippet.expectRequestParameters("missing-optional-request-parameter")