From 7bcfbd9e35ebcb025ceb67876362934c0289dc16 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 27 Oct 2016 10:16:46 +0100 Subject: [PATCH] Add support for documenting a portion of a request or response payload Closes gh-312 --- .../docs/asciidoc/documenting-your-api.adoc | 63 ++ docs/src/test/java/com/example/Payload.java | 10 + .../java/com/example/mockmvc/Payload.java | 31 +- .../java/com/example/restassured/Payload.java | 12 + .../payload/AbstractFieldsSnippet.java | 91 ++- .../FieldPathPayloadSubsectionExtractor.java | 108 +++ .../payload/PayloadDocumentation.java | 675 ++++++++++++++++++ .../payload/PayloadHandlingException.java | 15 +- .../payload/PayloadSubsectionExtractor.java | 55 ++ .../payload/RequestFieldsSnippet.java | 73 +- .../payload/RequestPartFieldsSnippet.java | 76 +- .../payload/ResponseFieldsSnippet.java | 76 +- ...ldPathPayloadSubsectionExtractorTests.java | 110 +++ .../payload/RequestFieldsSnippetTests.java | 28 + .../RequestPartFieldsSnippetTests.java | 20 + .../payload/ResponseFieldsSnippetTests.java | 14 + 16 files changed, 1425 insertions(+), 32 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 4b3d58fa..557a13d2 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -402,6 +402,69 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] <1> Document the array <2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` +[[documenting-your-api-request-response-payloads-subsections]] +==== Documenting a portion of a request or response payload + +If a payload is large or structurally complex, it can be useful to document +individual sections of the payload. REST Docs allows you to do so by extracting a +subsection of the payload and then documenting it. + +Consider the following JSON response payload: + +[source,json,indent=0] +---- + { + "weather": { + "wind": { + "speed": 15.3, + "direction": 287.0 + }, + "temperature": { + "high": 21.2, + "low": 14.8 + } + } + } +---- + +A snippet that documents the fields of the `temperature` object (`high` and `low`) can +be produced as follows: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload + beneath the path `weather.temperature`. Uses the static `beneathPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload + beneath the path `weather.temperature`. Uses the static `beneathPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + +The result is a snippet that contains a table describing the `high` and `low` fields +of `weather.temperature`. To make the snippet's name distinct, an identifier for the +subsection is included. By default, this identifier is `beneath-${path}`. For +example, the code above will result in a snippet named +`response-fields-beneath-weather.temperature.adoc`. The identifier can be customized using +the `withSubsectionId(String)` method: + +---- +include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] +---- + +This example will result in a snippet named `response-fields-temp.adoc`. + + + [[documenting-your-api-request-parameters]] === Request parameters diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index d33de4d1..f553927d 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -18,7 +18,9 @@ package com.example; import org.springframework.restdocs.payload.FieldDescriptor; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; public class Payload { @@ -31,4 +33,12 @@ public class Payload { // end::book-descriptors[] } + public void customSubsectionId() { + // tag::custom-subsection-id[] + responseFields(beneathPath("weather.temperature").withSubsectionId("temp"), + fieldWithPath("high").description("…"), + fieldWithPath("low").description("…")); + // end::custom-subsection-id[] + } + } diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 615354a3..9701f29f 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -16,21 +16,22 @@ package com.example.mockmvc; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; -import static org.springframework.restdocs.snippet.Attributes.attributes; -import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - import org.springframework.http.MediaType; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + public class Payload { private MockMvc mockMvc; @@ -91,4 +92,14 @@ public class Payload { // end::book-array[] } + public void subsection() throws Exception { + // tag::subsection[] + this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))); + // end::subsection[] + } + } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index 155e357e..d54a7e6d 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -23,6 +23,7 @@ import com.jayway.restassured.RestAssured; import com.jayway.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -95,4 +96,15 @@ public class Payload { // end::book-array[] } + public void subsection() throws Exception { + // tag::subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))) + .when().get("/locations/1") + .then().assertThat().statusCode(is(200)); + // end::subsection[] + } + } 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 ae6fb3fe..a630cc38 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 @@ -45,6 +45,8 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private final String type; + private final PayloadSubsectionExtractor subsectionExtractor; + /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields}. The fields will be documented using the given @@ -81,6 +83,29 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { this(type, type, descriptors, attributes, ignoreUndocumentedFields); } + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields in + * the subsection of the payload extracted by the given {@code subsectionExtractor} + * 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 + * @param subsectionExtractor the subsection extractor + * @since 1.2.0 + */ + protected AbstractFieldsSnippet(String type, List descriptors, + Map attributes, boolean ignoreUndocumentedFields, + PayloadSubsectionExtractor subsectionExtractor) { + this(type, type, descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); + } + /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields} using a template named {@code -fields}. The fields will @@ -98,7 +123,35 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { protected AbstractFieldsSnippet(String name, String type, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super(name + "-fields", type + "-fields", attributes); + this(name, type, descriptors, attributes, ignoreUndocumentedFields, null); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields in + * the subsection of the payload identified by {@code subsectionPath} 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 name the name of the snippet + * @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 + * @param subsectionExtractor the subsection extractor documented. {@code null} or an + * empty string can be used to indicate that the entire payload should be documented. + * @since 1.2.0 + */ + protected AbstractFieldsSnippet(String name, String type, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields, + PayloadSubsectionExtractor subsectionExtractor) { + super(name + "-fields" + + (subsectionExtractor != null + ? "-" + subsectionExtractor.getSubsectionId() : ""), + type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { @@ -112,11 +165,24 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { this.fieldDescriptors = descriptors; this.ignoreUndocumentedFields = ignoreUndocumentedFields; this.type = type; + this.subsectionExtractor = subsectionExtractor; } @Override protected Map createModel(Operation operation) { - ContentHandler contentHandler = getContentHandler(operation); + byte[] content; + try { + content = verifyContent(getContent(operation)); + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } + MediaType contentType = getContentType(operation); + if (this.subsectionExtractor != null) { + content = verifyContent( + this.subsectionExtractor.extractSubsection(content, contentType)); + } + ContentHandler contentHandler = getContentHandler(content, contentType); validateFieldDocumentation(contentHandler); @@ -146,28 +212,27 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { return model; } - private ContentHandler getContentHandler(Operation operation) { - MediaType contentType = getContentType(operation); - ContentHandler contentHandler; + private byte[] verifyContent(byte[] content) { + if (content.length == 0) { + throw new SnippetException("Cannot document " + this.type + " fields as the " + + this.type + " body is empty"); + } + return content; + } + private ContentHandler getContentHandler(byte[] content, MediaType contentType) { try { - byte[] content = getContent(operation); - if (content.length == 0) { - throw new SnippetException("Cannot document " + this.type - + " fields as the " + this.type + " body is empty"); - } if (contentType != null && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - contentHandler = new XmlContentHandler(content); + return new XmlContentHandler(content); } else { - contentHandler = new JsonContentHandler(content); + return new JsonContentHandler(content); } } catch (IOException ex) { throw new ModelCreationException(ex); } - return contentHandler; } private void validateFieldDocumentation(ContentHandler payloadHandler) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java new file mode 100644 index 00000000..ad948e82 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java @@ -0,0 +1,108 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.http.MediaType; + +/** + * A {@link PayloadSubsectionExtractor} that extracts the subsection of the JSON payload + * identified by a field path. + * + * @author Andy Wilkinson + * @since 1.2.0 + * @see PayloadDocumentation#beneathPath(String) + */ +public class FieldPathPayloadSubsectionExtractor + implements PayloadSubsectionExtractor { + + private final String fieldPath; + + private final String subsectionId; + + /** + * Creates a new {@code FieldPathPayloadSubsectionExtractor} that will extract the + * subsection of the JSON payload found at the given {@code fieldPath}. The + * {@code fieldPath} prefixed with {@code beneath-} with be used as the subsection ID. + * + * @param fieldPath the path of the field + */ + protected FieldPathPayloadSubsectionExtractor(String fieldPath) { + this(fieldPath, "beneath-" + fieldPath); + } + + /** + * Creates a new {@code FieldPathPayloadSubsectionExtractor} that will extract the + * subsection of the JSON payload found at the given {@code fieldPath} and that will + * us the given {@code subsectionId} to identify the subsection. + * + * @param fieldPath the path of the field + * @param subsectionId the ID of the subsection + */ + protected FieldPathPayloadSubsectionExtractor(String fieldPath, String subsectionId) { + this.fieldPath = fieldPath; + this.subsectionId = subsectionId; + } + + @Override + public byte[] extractSubsection(byte[] payload, MediaType contentType) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + JsonFieldPath compiledPath = JsonFieldPath.compile(this.fieldPath); + Object extracted = new JsonFieldProcessor().extract(compiledPath, + objectMapper.readValue(payload, Object.class)); + if (extracted instanceof List && !compiledPath.isPrecise()) { + List extractedList = (List) extracted; + if (extractedList.size() == 1) { + extracted = extractedList.get(0); + } + else { + throw new PayloadHandlingException(this.fieldPath + + " does not uniquely identify a subsection of the payload"); + } + } + return objectMapper.writeValueAsBytes(extracted); + } + catch (IOException ex) { + throw new PayloadHandlingException(ex); + } + } + + @Override + public String getSubsectionId() { + return this.subsectionId; + } + + /** + * Returns the path of the field that will be extracted. + * + * @return the path of the field + */ + protected String getFieldPath() { + return this.fieldPath; + } + + @Override + public FieldPathPayloadSubsectionExtractor withSubsectionId(String subsectionId) { + return new FieldPathPayloadSubsectionExtractor(this.fieldPath, subsectionId); + } + +} 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 bb6bbbc2..286cd185 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 @@ -268,6 +268,216 @@ public abstract class PayloadDocumentation { return new RequestFieldsSnippet(descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of the subsection of API + * operations's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors}. + *

+ * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request, + * 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. + *

+ * 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 subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return requestFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields in the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. + *

+ * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request, + * 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. + *

+ * 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 subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . 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 subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedRequestFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . 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 subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * 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 present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * 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. + *

+ * 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 subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return requestFields(subsectionExtractor, attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * 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 present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * 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. + *

+ * 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 subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * 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 subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedRequestFields(subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * 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 subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, attributes, + true); + } + /** * Returns a {@code Snippet} that will document the fields of the specified * {@code part} of the API operations's request payload. The fields will be documented @@ -287,6 +497,7 @@ public abstract class PayloadDocumentation { * @param part the part name * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -452,6 +663,235 @@ public abstract class PayloadDocumentation { return new RequestPartFieldsSnippet(part, descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

+ * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

+ * 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 part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the subsection's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return requestPartFields(part, subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

+ * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

+ * 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 part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the subsection's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. 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 part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, subsectionExtractor, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. 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 part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the givne {@code subsectionExtractor}. 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 present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

+ * 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 part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return requestPartFields(part, subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. 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 present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

+ * 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 part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, + attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. 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 part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. 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 part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, 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} @@ -494,7 +934,9 @@ public abstract class PayloadDocumentation { * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( List descriptors) { @@ -511,7 +953,9 @@ public abstract class PayloadDocumentation { * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( FieldDescriptor... descriptors) { @@ -623,6 +1067,225 @@ public abstract class PayloadDocumentation { return new ResponseFieldsSnippet(descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

+ * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * 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. + *

+ * 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 subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return responseFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

+ * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * 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. + *

+ * 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 subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. 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 subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedResponseFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. 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 subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. 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 present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * 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. + *

+ * 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 subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return responseFields(subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. 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 present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * 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. + *

+ * 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 subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. 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 subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedResponseFields(subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. 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 subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, attributes, + true); + } + /** * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} * applied to their paths. @@ -651,6 +1314,18 @@ public abstract class PayloadDocumentation { return prefixedDescriptors; } + /** + * Returns a {@link PayloadSubsectionExtractor} that will extract the subsection of + * the JSON payload found beneath the given {@code path}. + * + * @param path the path + * @return the subsection extractor + * @since 1.2.0 + */ + public static PayloadSubsectionExtractor beneathPath(String path) { + return new FieldPathPayloadSubsectionExtractor(path); + } + private static Attribute[] asArray(Map attributeMap) { List attributes = new ArrayList<>(); for (Map.Entry attribute : attributeMap.entrySet()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java index 383ba222..6619f5ae 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.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. @@ -26,7 +26,18 @@ package org.springframework.restdocs.payload; class PayloadHandlingException extends RuntimeException { /** - * Creates a new {@code PayloadHandlingException} with the given cause. + * Creates a new {@code PayloadHandlingException} with the given {@code message}. + * + * @param message the message + * @since 1.2.0 + */ + PayloadHandlingException(String message) { + super(message); + } + + /** + * Creates a new {@code PayloadHandlingException} with the given {@code cause}. + * * @param cause the cause of the failure */ PayloadHandlingException(Throwable cause) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java new file mode 100644 index 00000000..c69989ba --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java @@ -0,0 +1,55 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.payload; + +import org.springframework.http.MediaType; + +/** + * Strategy interface for extracting a subsection of a payload. + * + * @param The subsection extractor subclass + * @author Andy Wilkinson + * @since 1.2.0 + */ +public interface PayloadSubsectionExtractor> { + + /** + * Extracts a subsection of the given {@code payload} that has the given + * {@code contentType}. + * + * @param payload the payload + * @param contentType the content type of the payload + * @return the subsection of the payload + */ + byte[] extractSubsection(byte[] payload, MediaType contentType); + + /** + * Returns an identifier for the subsection that this extractor will extract. + * + * @return the identifier + */ + String getSubsectionId(); + + /** + * Returns an extractor with the given {@code subsectionId}. + * + * @param subsectionId the subsection ID + * @return the customized extractor + */ + T withSubsectionId(String subsectionId); + +} 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 ea8f68b2..9be9e32c 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 @@ -86,7 +86,74 @@ public class RequestFieldsSnippet extends AbstractFieldsSnippet { */ protected RequestFieldsSnippet(List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super("request", descriptors, attributes, ignoreUndocumentedFields); + this(null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. Undocumented fields will trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. If {@code ignoreUndocumentedFields} is {@code true}, + * undocumented fields will be ignored and will not trigger a failure. + * + * @param subsectionExtractor the subsection extractor document + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. The given {@code attributes} will be included in the + * model during template rendering. Undocumented fields will trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} 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 subsectionExtractor the path identifying the subsection of the payload to + * document + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + super("request", descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); } @Override @@ -137,8 +204,8 @@ public class RequestFieldsSnippet extends AbstractFieldsSnippet { FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, + Arrays.asList(additionalDescriptors))); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java index f07923de..1eb5205d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -33,6 +33,7 @@ import org.springframework.restdocs.snippet.SnippetException; * * @author Mathieu Pousse * @author Andy Wilkinson + * @since 1.2.0 * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) * @see PayloadDocumentation#requestPartFields(String, List) */ @@ -97,8 +98,81 @@ public class RequestPartFieldsSnippet extends AbstractFieldsSnippet { */ protected RequestPartFieldsSnippet(String partName, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { + this(partName, null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. Undocumented fields + * will trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(partName, subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(partName, subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. + * Undocumented fields will trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(partName, subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. 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 partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { super("request-part-" + partName, "request-part", descriptors, attributes, - ignoreUndocumentedFields); + ignoreUndocumentedFields, subsectionExtractor); this.partName = partName; } 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 cabe2948..79d306ea 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 @@ -87,7 +87,77 @@ public class ResponseFieldsSnippet extends AbstractFieldsSnippet { */ protected ResponseFieldsSnippet(List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super("response", descriptors, attributes, ignoreUndocumentedFields); + this(null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. Undocumented fields will + * trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. + * Undocumented fields will trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. 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 subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + super("response", descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); } @Override @@ -138,8 +208,8 @@ public class ResponseFieldsSnippet extends AbstractFieldsSnippet { FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, + Arrays.asList(additionalDescriptors))); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java new file mode 100644 index 00000000..eabc887a --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java @@ -0,0 +1,110 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.http.MediaType; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link FieldPathPayloadSubsectionExtractor}. + * + * @author Andy Wilkinson + */ +public class FieldPathPayloadSubsectionExtractorTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + @SuppressWarnings("unchecked") + public void extractMapSubsectionOfJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.b") + .extractSubsection("{\"a\":{\"b\":{\"c\":5}}}".getBytes(), + MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, + Map.class); + assertThat(extracted.size(), is(equalTo(1))); + assertThat(extracted.get("c"), is(equalTo((Object) 5))); + } + + @Test + @SuppressWarnings("unchecked") + public void extractMultiElementArraySubsectionOfJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a") + .extractSubsection("{\"a\":[{\"b\":5},{\"b\":4}]}".getBytes(), + MediaType.APPLICATION_JSON); + List> extracted = new ObjectMapper() + .readValue(extractedPayload, List.class); + assertThat(extracted.size(), is(equalTo(2))); + assertThat(extracted.get(0).get("b"), is(equalTo((Object) 5))); + assertThat(extracted.get(1).get("b"), is(equalTo((Object) 4))); + } + + @Test + @SuppressWarnings("unchecked") + public void extractSingleElementArraySubsectionOfJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[]") + .extractSubsection("{\"a\":[{\"b\":5}]}".getBytes(), + MediaType.APPLICATION_JSON); + List> extracted = new ObjectMapper() + .readValue(extractedPayload, List.class); + assertThat(extracted.size(), is(equalTo(1))); + assertThat(extracted.get(0).get("b"), is(equalTo((Object) 5))); + } + + @Test + @SuppressWarnings("unchecked") + public void extractMapSubsectionFromSingleElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b") + .extractSubsection("{\"a\":[{\"b\":{\"c\":5}}]}".getBytes(), + MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, + Map.class); + assertThat(extracted.size(), is(equalTo(1))); + assertThat(extracted.get("c"), is(equalTo((Object) 5))); + } + + @Test + public void extractMapSubsectionFromMultiElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + this.thrown.expect(PayloadHandlingException.class); + this.thrown.expectMessage( + equalTo("a.[].b does not uniquely identify a subsection of the payload")); + new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection( + "{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6}}]}".getBytes(), + MediaType.APPLICATION_JSON); + } + +} 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 8e6d8414..d3ad4e25 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 @@ -33,7 +33,9 @@ import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -63,6 +65,19 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests { .build()); } + @Test + public void subsectionOfMapRequest() throws IOException { + this.snippets.expect("request-fields-beneath-a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + + requestFields(beneathPath("a"), fieldWithPath("b").description("one"), + fieldWithPath("c").description("two")) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + } + @Test public void arrayRequestWithFields() throws IOException { this.snippets.expectRequestFields() @@ -81,6 +96,19 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests { .build()); } + @Test + public void subsectionOfArrayRequest() throws IOException { + this.snippets.expect("request-fields-beneath-[].a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + + requestFields(beneathPath("[].a"), fieldWithPath("b").description("one"), + fieldWithPath("c").description("two")) + .document(this.operationBuilder.request("http://localhost") + .content("[{\"a\": {\"b\": 5, \"c\": \"charlie\"}}]") + .build()); + } + @Test public void ignoredRequestField() throws IOException { this.snippets.expectRequestFields() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java index 0d4c7c32..8a1334ee 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java @@ -26,6 +26,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.templates.TemplateFormat; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; /** @@ -60,6 +61,25 @@ public class RequestPartFieldsSnippetTests extends AbstractSnippetTests { .build()); } + @Test + public void mapRequestPartSubsectionFields() throws IOException { + this.snippets.expect("request-part-one-fields-beneath-a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + + new RequestPartFieldsSnippet("one", + beneathPath("a"), Arrays + .asList(fieldWithPath("b").description("one"), + fieldWithPath("c").description("two"))) + .document( + this.operationBuilder + .request("http://localhost") + .part("one", + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}" + .getBytes()) + .build()); + } + @Test public void multipleRequestParts() throws IOException { this.snippets.expectRequestPartFields("one"); 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 59002896..3b8c1728 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 @@ -33,7 +33,9 @@ import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -70,6 +72,18 @@ public class ResponseFieldsSnippetTests extends AbstractSnippetTests { .build()); } + @Test + public void subsectionOfMapResponse() throws IOException { + this.snippets.expect("response-fields-beneath-a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + responseFields(beneathPath("a"), fieldWithPath("b").description("one"), + fieldWithPath("c").description("two")) + .document(this.operationBuilder.response() + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + } + @Test public void arrayResponseWithFields() throws IOException { this.snippets.expectResponseFields()