From dec3727da1c6a333bb17db4cfc8f15bb7d3bf99d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 31 Oct 2016 12:46:36 +0000 Subject: [PATCH] Add support for documenting body of a request, response or request part Closes gh-318 Closes gh-319 --- .../docs/asciidoc/documenting-your-api.adoc | 178 ++++++++++++++---- docs/src/docs/asciidoc/getting-started.adoc | 4 +- docs/src/test/java/com/example/Payload.java | 6 +- .../java/com/example/mockmvc/Payload.java | 93 +++++---- .../example/mockmvc/RequestPartPayload.java | 21 ++- .../java/com/example/restassured/Payload.java | 18 +- .../restassured/RequestPartPayload.java | 19 +- .../restdocs/config/SnippetConfigurer.java | 4 +- .../restdocs/payload/AbstractBodySnippet.java | 125 ++++++++++++ .../payload/PayloadDocumentation.java | 158 ++++++++++++++++ .../restdocs/payload/RequestBodySnippet.java | 84 +++++++++ .../payload/RequestPartBodySnippet.java | 111 +++++++++++ .../restdocs/payload/ResponseBodySnippet.java | 84 +++++++++ .../asciidoctor/default-request-body.snippet | 4 + .../default-request-part-body.snippet | 4 + .../asciidoctor/default-response-body.snippet | 4 + .../markdown/default-request-body.snippet | 3 + .../default-request-part-body.snippet | 3 + .../markdown/default-response-body.snippet | 3 + .../restdocs/AbstractSnippetTests.java | 6 +- .../RestDocumentationConfigurerTests.java | 10 +- .../payload/RequestBodyPartSnippetTests.java | 95 ++++++++++ .../payload/RequestBodySnippetTests.java | 92 +++++++++ .../payload/ResponseBodySnippetTests.java | 92 +++++++++ .../restdocs/test/SnippetMatchers.java | 13 +- .../request-body-with-language.snippet | 4 + .../request-part-body-with-language.snippet | 4 + .../response-body-with-language.snippet | 4 + .../request-body-with-language.snippet | 3 + .../request-part-body-with-language.snippet | 3 + .../response-body-with-language.snippet | 3 + 31 files changed, 1160 insertions(+), 95 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-part-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-part-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-body-with-language.snippet diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index a1eeb908..e6068ce7 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -105,12 +105,28 @@ descriptors to a snippet that's preconfigured to ignore certain links. For examp include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links] ---- + + [[documenting-your-api-request-response-payloads]] === Request and response payloads In addition to the hypermedia-specific support <>, support for general documentation of request and response payloads is also -provided. Consider the following payload: +provided. + +By default, Spring REST Docs will automatically generate snippets for the body of the +request and the body of the response. These snippets are named `request-body.adoc` and +`response-body.adoc` respectively. + + + +[[documenting-your-api-request-response-payloads-fields]] +==== Request and response fields + +To provide more detailed documentation of a request or response payload, support for +documenting the payload's fields is provided. + +Consider the following payload: [source,json,indent=0] ---- @@ -122,7 +138,7 @@ provided. Consider the following payload: } ---- -It can be documented like this: +Its fields can be documented like this: [source,java,indent=0,role="primary"] .MockMvc @@ -195,11 +211,11 @@ TIP: By default, Spring REST Docs will assume that the payload you are documenti JSON. If you want to document an XML payload the content type of the request or response must be compatible with `application/xml`. -[[documenting-your-api-request-response-payloads-json]] -==== JSON payloads +[[documenting-your-api-request-response-payloads-fields-json]] +===== Fields in JSON payloads -[[documenting-your-api-request-response-payloads-json-field-paths]] -===== JSON field paths +[[documenting-your-api-request-response-payloads-fields-json-field-paths]] +====== JSON field paths JSON field paths use either dot notation or bracket notation. Dot notation uses '.' to separate each key in the path; `a.b`, for example. Bracket notation wraps each key in @@ -288,8 +304,8 @@ found in the following array: -[[documenting-your-api-request-response-payloads-json-field-types]] -===== JSON field types +[[documenting-your-api-request-response-payloads-fields-json-field-types]] +====== JSON field types When a field is documented, Spring REST Docs will attempt to determine its type by examining the payload. Seven different types are supported: @@ -340,18 +356,18 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=explicit-type] <1> Set the field's type to `string`. -[[documenting-your-api-request-response-payloads-xml]] -==== XML payloads +[[documenting-your-api-request-response-payloads-fields-xml]] +===== XML payloads -[[documenting-your-api-request-response-payloads-xml-field-paths]] -===== XML field paths +[[documenting-your-api-request-response-payloads-fields-xml-field-paths]] +====== XML field paths XML field paths are described using XPath. `/` is used to descend into a child node. -[[documenting-your-api-request-response-payloads-xml-field-types]] -===== XML field types +[[documenting-your-api-request-response-payloads-fields-xml-field-types]] +====== XML field types When documenting an XML payload, you must provide a type for the field using the `type(Object)` method on `FieldDescriptor`. The result of the supplied type's `toString` @@ -359,8 +375,8 @@ method will be used in the documentation. -[[documenting-your-api-request-response-payloads-reusing-field-descriptors]] -==== Reusing field descriptors +[[documenting-your-api-request-response-payloads-fields-reusing-field-descriptors]] +===== Reusing field descriptors In addition to the general support for <>, the request and response snippets allow additional descriptors to be @@ -445,7 +461,10 @@ 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: +[[documenting-your-api-request-response-payloads-subsections-body]] +===== Documenting a subsection of a request or response body + +Consider the following JSON response body: [source,json,indent=0] ---- @@ -463,13 +482,64 @@ Consider the following JSON response payload: } ---- -A snippet that documents the fields of the `temperature` object (`high` and `low`) can -be produced as follows: +A snippet that documents the `temperature` object can be produces as follows: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path] +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. Uses the static + `responseBody` and `beneathPath` methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet + for the request body, `requestBody` can be used in place of `responseBody`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. Uses the static + `responseBody` and `beneathPath` methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet + for the request body, `requestBody` can be used in place of `responseBody`. + +The result is a snippet with the following contents: + +[source,json,indent=0] +---- + { + "temperature": { + "high": 21.2, + "low": 14.8 + } + } +---- + +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-body-beneath-weather.temperature.adoc`. The identifier can +be customized using the `withSubsectionId(String)` method: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] +---- + +This example will result in a snippet named `request-body-temp.adoc`. + + + +[[documenting-your-api-request-response-payloads-subsections-fields]] +===== Documenting the fields of a subsection of a request or response + +As well as documenting a subsection of a request or response body, it's also possible to +document the fields in a particular subsection. 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=fields-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 @@ -479,7 +549,7 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path] [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/Payload.java[tags=beneath-path] +include::{examples-dir}/com/example/restassured/Payload.java[tags=fields-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 @@ -490,15 +560,7 @@ The result is a snippet that contains a table describing the `high` and `low` fi 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: - -[source,java,indent=0] ----- -include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] ----- - -This example will result in a snippet named `response-fields-temp.adoc`. +`response-fields-beneath-weather.temperature.adoc`. @@ -679,12 +741,51 @@ above. === Request part payloads The payload of a request part can be documented in much the same way as the -<>: +<> with support +for documenting a request part's body and its fields. + + + +[[documenting-your-api-request-parts-payloads-fields]] +==== Documenting a request part's body + +A snippet containing the body of a request part can be generated: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=payload] +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the + of the request part named `metadata`. Uses the static `requestPartBody` method on + `PayloadDocumentation`. + payload. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the request + part named `metadata`. Uses the static `requestPartBody` method on + `PayloadDocumentation`. + +The result is a snippet `request-part-${part-name}-body.adoc` that contains the part's +body. For example, documenting a part named `metadata` will produce a snippet named +`request-part-metadata-body.adoc`. + + + +[[documenting-your-api-request-parts-payloads-fields]] +==== Documenting a request part's fields + +A request part's fields can be documented in much the same way as the fields of a request +or response: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=fields] ---- <1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. Uses the static `requestPartFields` method on @@ -696,7 +797,7 @@ include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=payload [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=payload] +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=fields] ---- <1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. Uses the static `requestPartFields` method on @@ -930,6 +1031,13 @@ documented | `http-response.adoc` | Contains the HTTP response that was returned + +| `request-body.adoc` +| Contains the body of the request that was sent + +| `response-body.adoc` +| Contains the body of the response that was returned + |=== You can configure which snippets are produced by default. Please refer to the @@ -1076,6 +1184,4 @@ and adds a title: ---- <1> Add a title to the table <2> Add a new column named "Constraints" -<3> Include the descriptors' `constraints` attribute in each row of the table - - +<3> Include the descriptors' `constraints` attribute in each row of the table \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index d70a78ce..f84f0f66 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -406,12 +406,14 @@ static `document` method on <4> Invoke the root (`/`) of the service. <5> Assert that the service produce the expected response. -By default, four snippets are written: +By default, six snippets are written: * `/index/curl-request.adoc` * `/index/http-request.adoc` * `/index/http-response.adoc` * `/index/httpie-request.adoc` + * `/index/request-body.adoc` + * `/index/response-body.adoc` Refer to <> for more information about these and other snippets that can be produced by Spring REST Docs. diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index f553927d..eb98f69f 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -20,7 +20,7 @@ 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; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; public class Payload { @@ -35,9 +35,7 @@ public class Payload { public void customSubsectionId() { // tag::custom-subsection-id[] - responseFields(beneathPath("weather.temperature").withSubsectionId("temp"), - fieldWithPath("high").description("…"), - fieldWithPath("low").description("…")); + responseBody(beneathPath("weather.temperature").withSubsectionId("temp")); // 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 5802e260..d1b12200 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -26,9 +26,10 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuild 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.subsectionWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; 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; @@ -40,46 +41,50 @@ public class Payload { public void response() throws Exception { // tag::response[] this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("index", responseFields( // <1> - fieldWithPath("contact.email").description("The user's email address"), // <2> - fieldWithPath("contact.name").description("The user's name")))); // <3> + .andExpect(status().isOk()) + .andDo(document("index", + responseFields( // <1> + fieldWithPath("contact.email") + .description("The user's email address"), // <2> + fieldWithPath("contact.name").description("The user's name")))); // <3> // end::response[] } - + public void subsection() throws Exception { // tag::subsection[] this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("index", responseFields( // <1> - subsectionWithPath("contact").description("The user's contact details")))); // <1> + .andExpect(status().isOk()) + .andDo(document("index", + responseFields( // <1> + subsectionWithPath("contact") + .description("The user's contact details")))); // <1> // end::subsection[] } public void explicitType() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - // tag::explicit-type[] - .andDo(document("index", responseFields( - fieldWithPath("contact.email") - .type(JsonFieldType.STRING) // <1> - .description("The user's email address")))); - // end::explicit-type[] + .andExpect(status().isOk()) + // tag::explicit-type[] + .andDo(document("index", + responseFields( + fieldWithPath("contact.email").type(JsonFieldType.STRING) // <1> + .description("The user's email address")))); + // end::explicit-type[] } public void constraints() throws Exception { this.mockMvc.perform(post("/users/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - // tag::constraints[] - .andDo(document("create-user", requestFields( - attributes(key("title").value("Fields for user creation")), // <1> - fieldWithPath("name").description("The user's name") - .attributes(key("constraints") - .value("Must not be null. Must not be empty")), // <2> - fieldWithPath("email").description("The user's email address") - .attributes(key("constraints") - .value("Must be a valid email address"))))); // <3> - // end::constraints[] + .andExpect(status().isOk()) + // tag::constraints[] + .andDo(document("create-user", requestFields( + attributes(key("title").value("Fields for user creation")), // <1> + fieldWithPath("name").description("The user's name") + .attributes(key("constraints") + .value("Must not be null. Must not be empty")), // <2> + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints") + .value("Must be a valid email address"))))); // <3> + // end::constraints[] } public void descriptorReuse() throws Exception { @@ -89,27 +94,39 @@ public class Payload { // tag::single-book[] this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("book", responseFields(book))); // <1> + .andExpect(status().isOk()).andDo(document("book", responseFields(book))); // <1> // end::single-book[] // tag::book-array[] this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("book", responseFields( - fieldWithPath("[]").description("An array of books")) // <1> - .andWithPrefix("[].", book))); // <2> + .andDo(document("book", + responseFields( + fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))); // <2> // end::book-array[] } - public void subsectionBeneathPath() throws Exception { - // tag::beneath-path[] + public void fieldsSubsection() throws Exception { + // tag::fields-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::beneath-path[] + .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::fields-subsection[] + } + + public void bodySubsection() throws Exception { + // tag::body-subsection[] + this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("location", + responseBody(beneathPath("weather.temperature")))); // <1> + + // end::body-subsection[] } } diff --git a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java index 56d6ae85..e020a4e7 100644 --- a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java +++ b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java @@ -23,6 +23,7 @@ import org.springframework.test.web.servlet.MockMvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.fileUpload; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -30,8 +31,8 @@ public class RequestPartPayload { private MockMvc mockMvc; - public void response() throws Exception { - // tag::payload[] + public void fields() throws Exception { + // tag::fields[] MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", "<>".getBytes()); MockMultipartFile metadata = new MockMultipartFile("metadata", "", @@ -42,7 +43,21 @@ public class RequestPartPayload { .andExpect(status().isOk()) .andDo(document("image-upload", requestPartFields("metadata", // <1> fieldWithPath("version").description("The version of the image")))); // <2> - // end::payload[] + // end::fields[] + } + + public void body() throws Exception { + // tag::body[] + MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", + "<>".getBytes()); + MockMultipartFile metadata = new MockMultipartFile("metadata", "", + "application/json", "{ \"version\": \"1.0\"}".getBytes()); + + this.mockMvc.perform(fileUpload("/images").file(image).file(metadata) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("image-upload", requestPartBody("metadata"))); // <1> + // end::body[] } } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index f7789cc5..b538acbb 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -26,6 +26,7 @@ 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.responseBody; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; @@ -46,7 +47,7 @@ public class Payload { .then().assertThat().statusCode(is(200)); // end::response[] } - + public void subsection() throws Exception { // tag::subsection[] RestAssured.given(this.spec).accept("application/json") @@ -107,15 +108,24 @@ public class Payload { // end::book-array[] } - public void subsectionBeneathPath() throws Exception { - // tag::beneath-path[] + public void fieldsSubsection() throws Exception { + // tag::fields-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::beneath-path[] + // end::fields-subsection[] + } + + public void bodySubsection() throws Exception { + // tag::body-subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseBody(beneathPath("weather.temperature")))) // <1> + .when().get("/locations/1") + .then().assertThat().statusCode(is(200)); + // end::body-subsection[] } } diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java index 5dc8eda5..ec83908e 100644 --- a/docs/src/test/java/com/example/restassured/RequestPartPayload.java +++ b/docs/src/test/java/com/example/restassured/RequestPartPayload.java @@ -25,6 +25,7 @@ import com.jayway.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; @@ -32,8 +33,8 @@ public class RequestPartPayload { private RequestSpecification spec; - public void response() throws Exception { - // tag::payload[] + public void fields() throws Exception { + // tag::fields[] Map metadata = new HashMap<>(); metadata.put("version", "1.0"); RestAssured.given(this.spec).accept("application/json") @@ -42,7 +43,19 @@ public class RequestPartPayload { .when().multiPart("image", new File("image.png"), "image/png") .multiPart("metadata", metadata).post("images") .then().assertThat().statusCode(is(200)); - // end::payload[] + // end::fields[] + } + + public void body() throws Exception { + // tag::body[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartBody("metadata"))) // <1> + .when().multiPart("image", new File("image.png"), "image/png") + .multiPart("metadata", metadata).post("images") + .then().assertThat().statusCode(is(200)); + // end::body[] } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 0da0b48c..9e96055c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -25,6 +25,7 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.cli.CliDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; +import org.springframework.restdocs.payload.PayloadDocumentation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; @@ -42,7 +43,8 @@ public abstract class SnippetConfigurer private List defaultSnippets = new ArrayList<>(Arrays.asList( CliDocumentation.curlRequest(), CliDocumentation.httpieRequest(), - HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse())); + HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse(), + PayloadDocumentation.requestBody(), PayloadDocumentation.responseBody())); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java new file mode 100644 index 00000000..0010ea28 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java @@ -0,0 +1,125 @@ +/* + * 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.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.ModelCreationException; +import org.springframework.restdocs.snippet.TemplatedSnippet; + +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response body. + * + * @author Andy Wilkinson + */ +public abstract class AbstractBodySnippet extends TemplatedSnippet { + + private final PayloadSubsectionExtractor subsectionExtractor; + + /** + * Creates a new {@code AbstractBodySnippet} that will produce a snippet named + * {@code -body} using a template named {@code -body}. The snippet will + * contain the subsection of the body extracted by the given + * {@code subsectionExtractor}. The given {@code attributes} will be included in the + * model during template rendering + * + * @param type the type of the body + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + */ + protected AbstractBodySnippet(String type, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + this(type, type, subsectionExtractor, attributes); + } + + /** + * Creates a new {@code AbstractBodySnippet} that will produce a snippet named + * {@code -body} using a template named {@code -body}. The snippet will + * contain the subsection of the body extracted by the given + * {@code subsectionExtractor}. The given {@code attributes} will be included in the + * model during template rendering + * + * @param name the name of the snippet + * @param type the type of the body + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + */ + protected AbstractBodySnippet(String name, String type, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super(name + "-body" + + (subsectionExtractor != null + ? "-" + subsectionExtractor.getSubsectionId() : ""), + type + "-body", attributes); + this.subsectionExtractor = subsectionExtractor; + } + + @Override + protected Map createModel(Operation operation) { + try { + MediaType contentType = getContentType(operation); + byte[] content = getContent(operation); + if (this.subsectionExtractor != null) { + content = this.subsectionExtractor.extractSubsection(content, + contentType); + } + Charset charset = extractCharset(contentType); + String body = charset != null ? new String(content, charset) + : new String(content); + Map model = new HashMap<>(); + model.put("body", body); + return model; + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } + } + + private Charset extractCharset(MediaType contentType) { + if (contentType == null) { + return null; + } + return contentType.getCharset(); + } + + /** + * Returns the content of the request or response extracted from the given + * {@code operation}. + * + * @param operation The operation + * @return The content + * @throws IOException if the content cannot be extracted + */ + protected abstract byte[] getContent(Operation operation) throws IOException; + + /** + * Returns the content type of the request or response extracted from the given + * {@code operation}. + * + * @param operation The operation + * @return The content type + */ + protected abstract MediaType getContentType(Operation operation); + +} 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 3f31ef76..7ecbf9eb 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 @@ -1443,6 +1443,164 @@ public abstract class PayloadDocumentation { true); } + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * request payload. + * + * @return the snippet that will document the request body + */ + public static RequestBodySnippet requestBody() { + return new RequestBodySnippet(); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * request payload. The given attributes will be made available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the request body + */ + public static RequestBodySnippet requestBody(Map attributes) { + return new RequestBodySnippet(attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's request payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the request body subsection + */ + public static RequestBodySnippet requestBody( + PayloadSubsectionExtractor subsectionExtractor) { + return new RequestBodySnippet(subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's request payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The given attributes will be made available during + * snippet generation. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the request body subsection + */ + public static RequestBodySnippet requestBody( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new RequestBodySnippet(subsectionExtractor, attributes); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * response payload. + * + * @return the snippet that will document the response body + */ + public static ResponseBodySnippet responseBody() { + return new ResponseBodySnippet(); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * response payload. The given attributes will be made available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static ResponseBodySnippet responseBody(Map attributes) { + return new ResponseBodySnippet(attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the response body subsection + */ + public static ResponseBodySnippet responseBody( + PayloadSubsectionExtractor subsectionExtractor) { + return new ResponseBodySnippet(subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The given attributes will be made available during + * snippet generation. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the response body subsection + */ + public static ResponseBodySnippet responseBody( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new ResponseBodySnippet(subsectionExtractor, attributes); + } + + /** + * Returns a {@code Snippet} that will document the body of specified part of the API + * operation's request payload. + * + * @param partName the name of the request part + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName) { + return new RequestPartBodySnippet(partName); + } + + /** + * Returns a {@code Snippet} that will document the body of specified part of the API + * operation's request payload. The given attributes will be made available during + * snippet generation. + * + * @param partName the name of the request part + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + Map attributes) { + return new RequestPartBodySnippet(partName, attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of specified + * part of the API operation's request payload. The subsection will be extracted using + * the given {@code subsectionExtractor}. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + PayloadSubsectionExtractor subsectionExtractor) { + return new RequestPartBodySnippet(partName, subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of specified + * part of the API operation's request payload. The subsection will be extracted using + * the given {@code subsectionExtractor}. The given attributes will be made available + * during snippet generation. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new RequestPartBodySnippet(partName, subsectionExtractor, attributes); + } + /** * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} * applied to their paths. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java new file mode 100644 index 00000000..5c6d3ea2 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java @@ -0,0 +1,84 @@ +/* + * 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.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the body of a request. + * + * @author Andy Wilkinson + */ +public class RequestBodySnippet extends AbstractBodySnippet { + + /** + * Creates a new {@code RequestBodySnippet}. + */ + public RequestBodySnippet() { + this(null, null); + } + + /** + * Creates a new {@code RequestBodySnippet} that will document the subsection of the + * request body extracted by the given {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + */ + public RequestBodySnippet(PayloadSubsectionExtractor subsectionExtractor) { + this(subsectionExtractor, null); + } + + /** + * Creates a new {@code RequestBodySnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + public RequestBodySnippet(Map attributes) { + this(null, attributes); + } + + /** + * Creates a new {@code RequestBodySnippet} that will document the subsection of the + * request body extracted by the given {@code subsectionExtractor}. The given + * additional {@code attributes} that will be included in the model during template + * rendering. + * + * @param subsectionExtractor the subsection extractor + * @param attributes The additional attributes + */ + public RequestBodySnippet(PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("request", subsectionExtractor, attributes); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return operation.getRequest().getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return operation.getRequest().getHeaders().getContentType(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java new file mode 100644 index 00000000..0e5ec324 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java @@ -0,0 +1,111 @@ +/* + * 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.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the body of a request part. + * + * @author Andy Wilkinson + */ +public class RequestPartBodySnippet extends AbstractBodySnippet { + + private final String partName; + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. + * + * @param partName the name of the request part + */ + public RequestPartBodySnippet(String partName) { + this(partName, null, null); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the subsection of + * the body of the request part with the given {@code partName} extracted by the given + * {@code subsectionExtractor}. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + */ + public RequestPartBodySnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor) { + this(partName, subsectionExtractor, null); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. The given additional + * {@code attributes} will be included in the model during template rendering. + * + * @param partName the name of the request part + * @param attributes the additional attributes + */ + public RequestPartBodySnippet(String partName, Map attributes) { + this(partName, null, attributes); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. The subsection of the body extracted + * by the given {@code subsectionExtractor} will be documented and the given + * additional {@code attributes} that will be included in the model during template + * rendering. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @param attributes The additional attributes + */ + public RequestPartBodySnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("request-part-" + partName, "request-part", subsectionExtractor, + attributes); + this.partName = partName; + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return findPart(operation).getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return findPart(operation).getHeaders().getContentType(); + } + + private OperationRequestPart findPart(Operation operation) { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate; + } + } + throw new SnippetException("A request part named '" + this.partName + + "' was not found in the request"); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java new file mode 100644 index 00000000..d0dbe140 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java @@ -0,0 +1,84 @@ +/* + * 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.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the body of a response. + * + * @author Andy Wilkinson + */ +public class ResponseBodySnippet extends AbstractBodySnippet { + + /** + * Creates a new {@code ResponseBodySnippet}. + */ + public ResponseBodySnippet() { + this(null, null); + } + + /** + * Creates a new {@code ResponseBodySnippet} that will document the subsection of the + * response body extracted by the given {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + */ + public ResponseBodySnippet(PayloadSubsectionExtractor subsectionExtractor) { + this(subsectionExtractor, null); + } + + /** + * Creates a new {@code ResponseBodySnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + public ResponseBodySnippet(Map attributes) { + this(null, attributes); + } + + /** + * Creates a new {@code ResponseBodySnippet} that will document the subsection of the + * response body extracted by the given {@code subsectionExtractor}. The given + * additional {@code attributes} that will be included in the model during template + * rendering. + * + * @param subsectionExtractor the subsection extractor + * @param attributes The additional attributes + */ + public ResponseBodySnippet(PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("response", subsectionExtractor, attributes); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return operation.getResponse().getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return operation.getResponse().getHeaders().getContentType(); + } + +} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet new file mode 100644 index 00000000..e6cb8e9b --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet new file mode 100644 index 00000000..e6cb8e9b --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet new file mode 100644 index 00000000..e6cb8e9b --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet new file mode 100644 index 00000000..b897d409 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet @@ -0,0 +1,3 @@ +``` +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet new file mode 100644 index 00000000..b897d409 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet @@ -0,0 +1,3 @@ +``` +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet new file mode 100644 index 00000000..b897d409 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet @@ -0,0 +1,3 @@ +``` +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index dbca4c0e..014caa4d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -68,7 +68,11 @@ public abstract class AbstractSnippetTests { } public CodeBlockMatcher codeBlock(String language) { - return SnippetMatchers.codeBlock(this.templateFormat, language); + return this.codeBlock(language, null); + } + + public CodeBlockMatcher codeBlock(String language, String options) { + return SnippetMatchers.codeBlock(this.templateFormat, language, options); } public TableMatcher tableWithHeader(String... headers) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 995a6ffb..4cc0b635 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -31,6 +31,8 @@ import org.springframework.restdocs.cli.HttpieRequestSnippet; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; +import org.springframework.restdocs.payload.RequestBodySnippet; +import org.springframework.restdocs.payload.ResponseBodySnippet; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; @@ -76,7 +78,9 @@ public class RestDocumentationConfigurerTests { contains(instanceOf(CurlRequestSnippet.class), instanceOf(HttpieRequestSnippet.class), instanceOf(HttpRequestSnippet.class), - instanceOf(HttpResponseSnippet.class))); + instanceOf(HttpResponseSnippet.class), + instanceOf(RequestBodySnippet.class), + instanceOf(ResponseBodySnippet.class))); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), instanceOf(SnippetConfiguration.class))); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration @@ -138,7 +142,9 @@ public class RestDocumentationConfigurerTests { contains(instanceOf(CurlRequestSnippet.class), instanceOf(HttpieRequestSnippet.class), instanceOf(HttpRequestSnippet.class), - instanceOf(HttpResponseSnippet.class), equalTo(snippet))); + instanceOf(HttpResponseSnippet.class), + instanceOf(RequestBodySnippet.class), + instanceOf(ResponseBodySnippet.class), equalTo(snippet))); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java new file mode 100644 index 00000000..f19daee4 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java @@ -0,0 +1,95 @@ +/* + * 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 org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +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.requestPartBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestPartBodySnippet}. + * + * @author Andy Wilkinson + */ +public class RequestBodyPartSnippetTests extends AbstractSnippetTests { + + public RequestBodyPartSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void requestPartWithBody() throws IOException { + this.snippets.expect("request-part-one-body") + .withContents(codeBlock(null, "nowrap").content("some content")); + + requestPartBody("one").document(this.operationBuilder.request("http://localhost") + .part("one", "some content".getBytes()).build()); + } + + @Test + public void requestPartWithNoBody() throws IOException { + this.snippets.expect("request-part-one-body") + .withContents(codeBlock(null, "nowrap").content("")); + requestPartBody("one").document(this.operationBuilder.request("http://localhost") + .part("one", new byte[0]).build()); + } + + @Test + public void subsectionOfRequestPartBody() throws IOException { + this.snippets.expect("request-part-one-body-beneath-a.b") + .withContents(codeBlock(null, "nowrap").content("{\"c\":5}")); + + requestPartBody("one", beneathPath("a.b")) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\":{\"b\":{\"c\":5}}}".getBytes()).build()); + } + + @Test + public void customSnippetAttributes() throws IOException { + this.snippets.expect("request-part-one-body") + .withContents(codeBlock("json", "nowrap").content("{\"a\":\"alpha\"}")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-part-body")) + .willReturn(snippetResource("request-part-body-with-language")); + requestPartBody("one", + attributes( + key("language").value("json"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .part("one", + "{\"a\":\"alpha\"}".getBytes()) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java new file mode 100644 index 00000000..e6bde76d --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java @@ -0,0 +1,92 @@ +/* + * 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 org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +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.requestBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestBodySnippet}. + * + * @author Andy Wilkinson + */ +public class RequestBodySnippetTests extends AbstractSnippetTests { + + public RequestBodySnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void requestWithBody() throws IOException { + this.snippets.expect("request-body") + .withContents(codeBlock(null, "nowrap").content("some content")); + + requestBody().document(this.operationBuilder.request("http://localhost") + .content("some content").build()); + } + + @Test + public void requestWithNoBody() throws IOException { + this.snippets.expect("request-body") + .withContents(codeBlock(null, "nowrap").content("")); + requestBody().document(this.operationBuilder.request("http://localhost").build()); + } + + @Test + public void subsectionOfRequestBody() throws IOException { + this.snippets.expect("request-body-beneath-a.b") + .withContents(codeBlock(null, "nowrap").content("{\"c\":5}")); + + requestBody(beneathPath("a.b")) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\":{\"b\":{\"c\":5}}}").build()); + } + + @Test + public void customSnippetAttributes() throws IOException { + this.snippets.expect("request-body") + .withContents(codeBlock("json", "nowrap").content("{\"a\":\"alpha\"}")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-body")) + .willReturn(snippetResource("request-body-with-language")); + requestBody( + attributes( + key("language").value("json"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content("{\"a\":\"alpha\"}").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java new file mode 100644 index 00000000..2b42539c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java @@ -0,0 +1,92 @@ +/* + * 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 org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +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.responseBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link ResponseBodySnippet}. + * + * @author Andy Wilkinson + */ +public class ResponseBodySnippetTests extends AbstractSnippetTests { + + public ResponseBodySnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void responseWithBody() throws IOException { + this.snippets.expect("response-body") + .withContents(codeBlock(null, "nowrap").content("some content")); + + PayloadDocumentation.responseBody().document( + this.operationBuilder.response().content("some content").build()); + } + + @Test + public void responseWithNoBody() throws IOException { + this.snippets.expect("response-body") + .withContents(codeBlock(null, "nowrap").content("")); + PayloadDocumentation.responseBody() + .document(this.operationBuilder.response().build()); + } + + @Test + public void subsectionOfResponseBody() throws IOException { + this.snippets.expect("response-body-beneath-a.b") + .withContents(codeBlock(null, "nowrap").content("{\"c\":5}")); + + responseBody(beneathPath("a.b")).document(this.operationBuilder.response() + .content("{\"a\":{\"b\":{\"c\":5}}}").build()); + } + + @Test + public void customSnippetAttributes() throws IOException { + this.snippets.expect("response-body") + .withContents(codeBlock("json", "nowrap").content("{\"a\":\"alpha\"}")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-body")) + .willReturn(snippetResource("response-body-with-language")); + responseBody( + attributes( + key("language").value("json"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response().content("{\"a\":\"alpha\"}") + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 287c933f..c5e8da20 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -95,6 +95,15 @@ public final class SnippetMatchers { return new MarkdownCodeBlockMatcher(language); } + @SuppressWarnings({ "rawtypes" }) + public static CodeBlockMatcher codeBlock(TemplateFormat format, String language, + String options) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorCodeBlockMatcher(language, options); + } + return new MarkdownCodeBlockMatcher(language); + } + private static abstract class AbstractSnippetContentMatcher extends BaseMatcher { @@ -186,7 +195,7 @@ public final class SnippetMatchers { protected AsciidoctorCodeBlockMatcher(String language, String options) { super(TemplateFormats.asciidoctor()); - this.addLine("[source," + language + this.addLine("[source" + (language == null ? "" : "," + language) + (options == null ? "" : ",options=\"" + options + "\"") + "]"); this.addLine("----"); this.addLine("----"); @@ -204,7 +213,7 @@ public final class SnippetMatchers { protected MarkdownCodeBlockMatcher(String language) { super(TemplateFormats.markdown()); - this.addLine("```" + language); + this.addLine("```" + (language == null ? "" : language)); this.addLine("```"); } diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-body-with-language.snippet new file mode 100644 index 00000000..0429ee57 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-body-with-language.snippet @@ -0,0 +1,4 @@ +[source,{{language}},options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-part-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-part-body-with-language.snippet new file mode 100644 index 00000000..0429ee57 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-part-body-with-language.snippet @@ -0,0 +1,4 @@ +[source,{{language}},options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-body-with-language.snippet new file mode 100644 index 00000000..0429ee57 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-body-with-language.snippet @@ -0,0 +1,4 @@ +[source,{{language}},options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-body-with-language.snippet new file mode 100644 index 00000000..f81732e9 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-body-with-language.snippet @@ -0,0 +1,3 @@ +```{{language}} +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-part-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-part-body-with-language.snippet new file mode 100644 index 00000000..f81732e9 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-part-body-with-language.snippet @@ -0,0 +1,3 @@ +```{{language}} +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-body-with-language.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-body-with-language.snippet new file mode 100644 index 00000000..f81732e9 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-body-with-language.snippet @@ -0,0 +1,3 @@ +```{{language}} +{{body}} +``` \ No newline at end of file