From e00734d693ba07f7149843fe8e6ff5851c092fda Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2015 13:49:44 +0000 Subject: [PATCH] Add support for generating an HTTP request documentation snippet Closes gh-13 --- README.md | 10 +- .../src/main/asciidoc/api-guide.asciidoc | 36 +-- .../asciidoc/getting-started-guide.asciidoc | 44 ++-- .../src/main/asciidoc/api-guide.asciidoc | 36 +-- .../asciidoc/getting-started-guide.asciidoc | 44 ++-- .../RestDocumentationResultHandler.java | 8 +- .../config/RestDocumentationConfigurer.java | 12 + .../restdocs/curl/CurlConfiguration.java | 35 --- .../restdocs/curl/CurlDocumentation.java | 210 +++-------------- .../curl/CurlSnippetResultHandler.java | 51 ---- .../restdocs/http/HttpDocumentation.java | 155 ++++++++++++ .../util/DocumentableHttpServletRequest.java | 223 ++++++++++++++++++ .../RestDocumentationIntegrationTests.java | 21 +- .../RestDocumentationConfigurerTests.java | 23 ++ .../restdocs/curl/CurlDocumentationTests.java | 87 +------ .../restdocs/http/HttpDocumentationTests.java | 175 ++++++++++++++ 16 files changed, 725 insertions(+), 445 deletions(-) delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java diff --git a/README.md b/README.md index 9bacefee..1bc9b39d 100644 --- a/README.md +++ b/README.md @@ -279,9 +279,9 @@ command for the request and the resulting response to files in a directory named `index` in the project's `build/generated-snippets/` directory. Three files will be written: - - `index/request.asciidoc` - - `index/response.asciidoc` - - `index/request-response.asciidoc` + - `index/curl-request.asciidoc` + - `index/http-request.asciidoc` + - `index/http-response.asciidoc` #### Parameterized output directories @@ -340,8 +340,8 @@ which the snippets are written. For example, to include both the request and res snippets described above: ``` -include::{generated}/index/request.asciidoc[] -include::{generated}/index/response.asciidoc[] +include::{generated}/index/curl-request.asciidoc[] +include::{generated}/index/http-response.asciidoc[] ``` ## Generating snippets in your IDE diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc index 63151d08..48a32aad 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc @@ -69,7 +69,7 @@ include::{generated}/error-example/response-fields.asciidoc[] For example, a request that attempts to apply a non-existent tag to a note will produce a `400 Bad Request` response: -include::{generated}/error-example/response.asciidoc[] +include::{generated}/error-example/http-response.asciidoc[] [[overview-hypermedia]] == Hypermedia @@ -103,7 +103,7 @@ include::{generated}/index-example/response-fields.asciidoc[] ==== Example response -include::{generated}/index-example/response.asciidoc[] +include::{generated}/index-example/http-response.asciidoc[] @@ -132,11 +132,11 @@ include::{generated}/notes-list-example/response-fields.asciidoc[] ==== Example request -include::{generated}/notes-list-example/request.asciidoc[] +include::{generated}/notes-list-example/curl-request.asciidoc[] ==== Example response -include::{generated}/notes-list-example/response.asciidoc[] +include::{generated}/notes-list-example/http-response.asciidoc[] @@ -151,11 +151,11 @@ include::{generated}/notes-create-example/request-fields.asciidoc[] ==== Example request -include::{generated}/notes-create-example/request.asciidoc[] +include::{generated}/notes-create-example/curl-request.asciidoc[] ==== Example response -include::{generated}/notes-create-example/response.asciidoc[] +include::{generated}/notes-create-example/http-response.asciidoc[] @@ -177,11 +177,11 @@ include::{generated}/tags-list-example/response-fields.asciidoc[] ==== Example request -include::{generated}/tags-list-example/request.asciidoc[] +include::{generated}/tags-list-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tags-list-example/response.asciidoc[] +include::{generated}/tags-list-example/http-response.asciidoc[] @@ -196,11 +196,11 @@ include::{generated}/tags-create-example/request-fields.asciidoc[] ==== Example request -include::{generated}/tags-create-example/request.asciidoc[] +include::{generated}/tags-create-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tags-create-example/response.asciidoc[] +include::{generated}/tags-create-example/http-response.asciidoc[] @@ -229,11 +229,11 @@ include::{generated}/note-get-example/response-fields.asciidoc[] ==== Example request -include::{generated}/note-get-example/request.asciidoc[] +include::{generated}/note-get-example/curl-request.asciidoc[] ==== Example response -include::{generated}/note-get-example/response.asciidoc[] +include::{generated}/note-get-example/http-response.asciidoc[] @@ -250,11 +250,11 @@ To leave an attribute of a note unchanged, any of the above may be omitted from ==== Example request -include::{generated}/note-update-example/request.asciidoc[] +include::{generated}/note-update-example/curl-request.asciidoc[] ==== Example response -include::{generated}/note-update-example/response.asciidoc[] +include::{generated}/note-update-example/http-response.asciidoc[] @@ -283,11 +283,11 @@ include::{generated}/tag-get-example/response-fields.asciidoc[] ==== Example request -include::{generated}/tag-get-example/request.asciidoc[] +include::{generated}/tag-get-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tag-get-example/response.asciidoc[] +include::{generated}/tag-get-example/http-response.asciidoc[] @@ -302,8 +302,8 @@ include::{generated}/tag-update-example/request-fields.asciidoc[] ==== Example request -include::{generated}/tag-update-example/request.asciidoc[] +include::{generated}/tag-update-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tag-update-example/response.asciidoc[] +include::{generated}/tag-update-example/http-response.asciidoc[] diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc index e7781f24..c9eac952 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.asciidoc @@ -42,12 +42,12 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/1/request.asciidoc[] +include::{generated}/index/1/curl-request.asciidoc[] This request should yield the following response in the http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -59,26 +59,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] To create a note, you need to execute a `POST` request to this URI including a JSON payload containing the title and body of the note: -include::{generated}/creating-a-note/1/request.asciidoc[] +include::{generated}/creating-a-note/1/curl-request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/creating-a-note/1/response.asciidoc[] +include::{generated}/creating-a-note/1/http-response.asciidoc[] To work with the newly created note you use the URI in the `Location` header. For example, you can access the note's details by performing a `GET` request: -include::{generated}/creating-a-note/2/request.asciidoc[] +include::{generated}/creating-a-note/2/curl-request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/creating-a-note/2/response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.asciidoc[] Note the `tags` link which we'll make use of later. @@ -92,26 +92,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/creating-a-note/3/request.asciidoc[] +include::{generated}/creating-a-note/3/curl-request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/creating-a-note/3/response.asciidoc[] +include::{generated}/creating-a-note/3/http-response.asciidoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/creating-a-note/4/request.asciidoc[] +include::{generated}/creating-a-note/4/curl-request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/creating-a-note/4/response.asciidoc[] +include::{generated}/creating-a-note/4/http-response.asciidoc[] @@ -132,24 +132,25 @@ with it. Once again we execute a `POST` request. However, this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/creating-a-note/5/request.asciidoc[] +include::{generated}/creating-a-note/5/curl-request.asciidoc[] Once again, the response's `Location` header tells us the URI of the newly created note: -include::{generated}/creating-a-note/5/response.asciidoc[] +include::{generated}/creating-a-note/5/http-response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/creating-a-note/6/request-response.asciidoc[] +include::{generated}/creating-a-note/6/curl-request.asciidoc[] +include::{generated}/creating-a-note/6/http-response.asciidoc[] To verify that the tag has been associated with the note, we can perform a `GET` request against the URI from the `tags` link: -include::{generated}/creating-a-note/7/request.asciidoc[] +include::{generated}/creating-a-note/7/curl-request.asciidoc[] The response embeds information about the tag that we've just associated with the note: -include::{generated}/creating-a-note/7/response.asciidoc[] +include::{generated}/creating-a-note/7/http-response.asciidoc[] @@ -159,17 +160,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's a body that contains the array of tags to be associated with the note. We'll used the URI of the untagged note that we created earlier: -include::{generated}/creating-a-note/8/request.asciidoc[] +include::{generated}/creating-a-note/8/curl-request.asciidoc[] This request should produce a `204 No Content` response: -include::{generated}/creating-a-note/8/response.asciidoc[] +include::{generated}/creating-a-note/8/http-response.asciidoc[] When we first created this note, we noted the tags link included in its details: -include::{generated}/creating-a-note/2/response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.asciidoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/creating-a-note/9/request-response.asciidoc[] +include::{generated}/creating-a-note/9/curl-request.asciidoc[] +include::{generated}/creating-a-note/9/http-response.asciidoc[] diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc index 35edc0f3..3919f86c 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc @@ -69,7 +69,7 @@ include::{generated}/error-example/response-fields.asciidoc For example, a request that attempts to apply a non-existent tag to a note will produce a `400 Bad Request` response: -include::{generated}/error-example/response.asciidoc[] +include::{generated}/error-example/http-response.asciidoc[] [[overview-hypermedia]] == Hypermedia @@ -103,7 +103,7 @@ include::{generated}/index-example/response-fields.asciidoc[] ==== Example response -include::{generated}/index-example/response.asciidoc[] +include::{generated}/index-example/http-response.asciidoc[] @@ -132,11 +132,11 @@ include::{generated}/notes-list-example/response-fields.asciidoc[] ==== Example request -include::{generated}/notes-list-example/request.asciidoc[] +include::{generated}/notes-list-example/curl-request.asciidoc[] ==== Example response -include::{generated}/notes-list-example/response.asciidoc[] +include::{generated}/notes-list-example/http-response.asciidoc[] @@ -151,11 +151,11 @@ include::{generated}/notes-create-example/request-fields.asciidoc[] ==== Example request -include::{generated}/notes-create-example/request.asciidoc[] +include::{generated}/notes-create-example/curl-request.asciidoc[] ==== Example response -include::{generated}/notes-create-example/response.asciidoc[] +include::{generated}/notes-create-example/http-response.asciidoc[] @@ -177,11 +177,11 @@ include::{generated}/tags-list-example/response-fields.asciidoc[] ==== Example request -include::{generated}/tags-list-example/request.asciidoc[] +include::{generated}/tags-list-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tags-list-example/response.asciidoc[] +include::{generated}/tags-list-example/http-response.asciidoc[] @@ -196,11 +196,11 @@ include::{generated}/tags-create-example/request-fields.asciidoc[] ==== Example request -include::{generated}/tags-create-example/request.asciidoc[] +include::{generated}/tags-create-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tags-create-example/response.asciidoc[] +include::{generated}/tags-create-example/http-response.asciidoc[] @@ -229,11 +229,11 @@ include::{generated}/note-get-example/response-fields.asciidoc[] ==== Example request -include::{generated}/note-get-example/request.asciidoc[] +include::{generated}/note-get-example/curl-request.asciidoc[] ==== Example response -include::{generated}/note-get-example/response.asciidoc[] +include::{generated}/note-get-example/http-response.asciidoc[] @@ -250,11 +250,11 @@ To leave an attribute of a note unchanged, any of the above may be omitted from ==== Example request -include::{generated}/note-update-example/request.asciidoc[] +include::{generated}/note-update-example/curl-request.asciidoc[] ==== Example response -include::{generated}/note-update-example/response.asciidoc[] +include::{generated}/note-update-example/http-response.asciidoc[] [[resources-tag]] @@ -282,11 +282,11 @@ include::{generated}/tag-get-example/response-fields.asciidoc[] ==== Example request -include::{generated}/tag-get-example/request.asciidoc[] +include::{generated}/tag-get-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tag-get-example/response.asciidoc[] +include::{generated}/tag-get-example/http-response.asciidoc[] @@ -301,8 +301,8 @@ include::{generated}/tag-update-example/request-fields.asciidoc[] ==== Example request -include::{generated}/tag-update-example/request.asciidoc[] +include::{generated}/tag-update-example/curl-request.asciidoc[] ==== Example response -include::{generated}/tag-update-example/response.asciidoc[] +include::{generated}/tag-update-example/http-response.asciidoc[] diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc index 8fccc65c..9134e852 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.asciidoc @@ -42,11 +42,11 @@ $ java -jar build/libs/*.jar You can check that the service is up and running by executing a simple request using cURL: -include::{generated}/index/1/request.asciidoc[] +include::{generated}/index/1/curl-request.asciidoc[] This request should yield the following response: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] Note the `_links` in the JSON response. They are key to navigating the API. @@ -58,26 +58,26 @@ Now that you've started the service and verified that it works, the next step is it to create a new note. As you saw above, the URI for working with notes is included as a link when you perform a `GET` request against the root of the service: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] To create a note you need to execute a `POST` request to this URI, including a JSON payload containing the title and body of the note: -include::{generated}/creating-a-note/1/request.asciidoc[] +include::{generated}/creating-a-note/1/curl-request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created note: -include::{generated}/creating-a-note/1/response.asciidoc[] +include::{generated}/creating-a-note/1/http-response.asciidoc[] To work with the newly created note you use the URI in the `Location` header. For example you can access the note's details by performing a `GET` request: -include::{generated}/creating-a-note/2/request.asciidoc[] +include::{generated}/creating-a-note/2/curl-request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/creating-a-note/2/response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.asciidoc[] Note the `note-tags` link which we'll make use of later. @@ -91,26 +91,26 @@ to tag a note, you must first create the tag. Referring back to the response for the service's index, the URI for working with tags is include as a link: -include::{generated}/index/1/response.asciidoc[] +include::{generated}/index/1/http-response.asciidoc[] To create a tag you need to execute a `POST` request to this URI, including a JSON payload containing the name of the tag: -include::{generated}/creating-a-note/3/request.asciidoc[] +include::{generated}/creating-a-note/3/curl-request.asciidoc[] The response from this request should have a status code of `201 Created` and contain a `Location` header whose value is the URI of the newly created tag: -include::{generated}/creating-a-note/3/response.asciidoc[] +include::{generated}/creating-a-note/3/http-response.asciidoc[] To work with the newly created tag you use the URI in the `Location` header. For example you can access the tag's details by performing a `GET` request: -include::{generated}/creating-a-note/4/request.asciidoc[] +include::{generated}/creating-a-note/4/curl-request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/creating-a-note/4/response.asciidoc[] +include::{generated}/creating-a-note/4/http-response.asciidoc[] @@ -131,24 +131,25 @@ with it. Once again we execute a `POST` request, but this time, in an array named tags, we include the URI of the tag we just created: -include::{generated}/creating-a-note/5/request.asciidoc[] +include::{generated}/creating-a-note/5/curl-request.asciidoc[] Once again, the response's `Location` header tells use the URI of the newly created note: -include::{generated}/creating-a-note/5/response.asciidoc[] +include::{generated}/creating-a-note/5/http-response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/creating-a-note/6/request-response.asciidoc[] +include::{generated}/creating-a-note/6/curl-request.asciidoc[] +include::{generated}/creating-a-note/6/http-response.asciidoc[] To see the note's tags, execute a `GET` request against the URI of the note's `note-tags` link: -include::{generated}/creating-a-note/7/request.asciidoc[] +include::{generated}/creating-a-note/7/curl-request.asciidoc[] The response shows that, as expected, the note has a single tag: -include::{generated}/creating-a-note/7/response.asciidoc[] +include::{generated}/creating-a-note/7/http-response.asciidoc[] @@ -158,17 +159,18 @@ An existing note can be tagged by executing a `PATCH` request against the note's a body that contains the array of tags to be associated with the note. We'll use the URI of the untagged note that we created earlier: -include::{generated}/creating-a-note/8/request.asciidoc[] +include::{generated}/creating-a-note/8/curl-request.asciidoc[] This request should produce a `204 No Content` response: -include::{generated}/creating-a-note/8/response.asciidoc[] +include::{generated}/creating-a-note/8/http-response.asciidoc[] When we first created this note, we noted the `note-tags` link included in its details: -include::{generated}/creating-a-note/2/response.asciidoc[] +include::{generated}/creating-a-note/2/http-response.asciidoc[] We can use that link now and execute a `GET` request to see that the note now has a single tag: -include::{generated}/creating-a-note/9/request-response.asciidoc[] +include::{generated}/creating-a-note/9/curl-request.asciidoc[] +include::{generated}/creating-a-note/9/http-response.asciidoc[] diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java index bd3b6359..11b55534 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -17,8 +17,8 @@ package org.springframework.restdocs; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse; +import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; +import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; @@ -53,8 +53,8 @@ public class RestDocumentationResultHandler implements ResultHandler { this.delegates = new ArrayList(); this.delegates.add(documentCurlRequest(this.outputDir)); - this.delegates.add(documentCurlResponse(this.outputDir)); - this.delegates.add(documentCurlRequestAndResponse(this.outputDir)); + this.delegates.add(documentHttpRequest(this.outputDir)); + this.delegates.add(documentHttpResponse(this.outputDir)); } @Override diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 481d26ea..57b37f98 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -21,6 +21,7 @@ import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; +import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; /** @@ -108,8 +109,19 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { request.setScheme(RestDocumentationConfigurer.this.scheme); request.setServerPort(RestDocumentationConfigurer.this.port); request.setServerName(RestDocumentationConfigurer.this.host); + configureContentLengthHeaderIfAppropriate(request); return request; } + + private void configureContentLengthHeaderIfAppropriate( + MockHttpServletRequest request) { + long contentLength = request.getContentLengthLong(); + if (contentLength > 0 + && !StringUtils.hasText(request.getHeader("Content-Length"))) { + request.addHeader("Content-Length", request.getContentLengthLong()); + } + } + }; } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java deleted file mode 100644 index c8c8c944..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlConfiguration.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2014-2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.curl; - -/** - * Configuration for documenting Curl requests and responses - * - * @author Andy Wilkinson - */ -class CurlConfiguration { - - private boolean includeResponseHeaders = true; - - boolean isIncludeResponseHeaders() { - return this.includeResponseHeaders; - } - - void setIncludeResponseHeaders(boolean includeResponseHeaders) { - this.includeResponseHeaders = includeResponseHeaders; - } -} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index afd3b86d..960ba88c 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -16,29 +16,21 @@ package org.springframework.restdocs.curl; -import static org.springframework.restdocs.util.IterableEnumeration.iterable; - import java.io.IOException; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Map; +import java.util.List; +import java.util.Map.Entry; -import javax.servlet.http.HttpServletRequest; - -import org.springframework.http.HttpStatus; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.snippet.DocumentationWriter; import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; -import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMethod; /** * Static factory methods for documenting a RESTful API as if it were being driven using * the cURL command-line utility. - * + * * @author Andy Wilkinson * @author Yann Le Guern */ @@ -50,58 +42,17 @@ public abstract class CurlDocumentation { /** * Produces a documentation snippet containing the request formatted as a cURL command - * + * * @param outputDir The directory to which snippet should be written * @return the handler that will produce the snippet */ - public static CurlSnippetResultHandler documentCurlRequest(String outputDir) { - return new CurlSnippetResultHandler(outputDir, "request") { + public static SnippetWritingResultHandler documentCurlRequest(String outputDir) { + return new SnippetWritingResultHandler(outputDir, "curl-request") { @Override public void handle(MvcResult result, DocumentationWriter writer) throws IOException { - writer.shellCommand(new CurlRequestDocumentationAction(writer, result, - getCurlConfiguration())); - } - }; - } - - /** - * Produces a documentation snippet containing the response formatted as the response - * to a cURL command - * - * @param outputDir The directory to which snippet should be written - * @return the handler that will produce the snippet - */ - public static CurlSnippetResultHandler documentCurlResponse(String outputDir) { - return new CurlSnippetResultHandler(outputDir, "response") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.codeBlock("http", new CurlResponseDocumentationAction(writer, - result, getCurlConfiguration())); - } - }; - } - - /** - * Produces a documentation snippet containing both the request formatted as a cURL - * command and the response formatted formatted s the response to a cURL command. - * - * @param outputDir The directory to which the snippet should be written - * @return the handler that will produce the snippet - */ - public static CurlSnippetResultHandler documentCurlRequestAndResponse(String outputDir) { - return new CurlSnippetResultHandler(outputDir, "request-response") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.shellCommand(new CurlRequestDocumentationAction(writer, result, - getCurlConfiguration())); - writer.codeBlock("http", new CurlResponseDocumentationAction(writer, - result, getCurlConfiguration())); + writer.shellCommand(new CurlRequestDocumentationAction(writer, result)); } }; } @@ -121,152 +72,55 @@ public abstract class CurlDocumentation { private final MvcResult result; - private final CurlConfiguration curlConfiguration; - - CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result, - CurlConfiguration curlConfiguration) { + CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result) { this.writer = writer; this.result = result; - this.curlConfiguration = curlConfiguration; } @Override public void perform() throws IOException { - MockHttpServletRequest request = this.result.getRequest(); + DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( + this.result.getRequest()); this.writer.print(String.format("curl %s://%s", request.getScheme(), - request.getServerName())); + request.getHost())); if (isNonStandardPort(request)) { - this.writer.print(String.format(":%d", request.getServerPort())); + this.writer.print(String.format(":%d", request.getPort())); } - this.writer.print(getRequestUriWithQueryString(request)); + this.writer.print(request.getRequestUriWithQueryString().replace("&", "\\&")); - if (this.curlConfiguration.isIncludeResponseHeaders()) { - this.writer.print(" -i"); - } + this.writer.print(" -i"); - if (!isGetRequest(request)) { + if (!request.isGetRequest()) { this.writer.print(String.format(" -X %s", request.getMethod())); } - for (String headerName : iterable(request.getHeaderNames())) { - for (String header : iterable(request.getHeaders(headerName))) { - this.writer - .print(String.format(" -H \"%s: %s\"", headerName, header)); + for (Entry> entry : request.getHeaders().entrySet()) { + for (String header : entry.getValue()) { + this.writer.print(String.format(" -H \"%s: %s\"", entry.getKey(), + header)); } } - if (request.getContentLengthLong() > 0) { - this.writer.print(String.format(" -d '%s'", getContent(request))); + if (request.getContentLength() > 0) { + this.writer + .print(String.format(" -d '%s'", request.getContentAsString())); } - else if (isPostRequest(request)) { - Map parameters = request.getParameterMap(); - if (!parameters.isEmpty()) { - this.writer.print(String - .format(" -d '%s'", toQueryString(parameters))); + else if (request.isPostRequest()) { + String queryString = request.getParameterMapAsQueryString(); + if (StringUtils.hasText(queryString)) { + this.writer.print(String.format(" -d '%s'", + queryString.replace("&", "\\&"))); } } this.writer.println(); } - private boolean isGetRequest(HttpServletRequest request) { - return RequestMethod.GET == RequestMethod.valueOf(request.getMethod()); - } - - private boolean isPostRequest(HttpServletRequest request) { - return RequestMethod.POST == RequestMethod.valueOf(request.getMethod()); - } - - private boolean isNonStandardPort(HttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTP) - || (SCHEME_HTTPS.equals(request.getScheme()) && request - .getServerPort() != STANDARD_PORT_HTTPS); - } - - private String getRequestUriWithQueryString(HttpServletRequest request) { - StringBuilder sb = new StringBuilder(); - sb.append(request.getRequestURI()); - String queryString = getQueryString(request); - if (StringUtils.hasText(queryString)) { - sb.append('?').append(queryString); - } - return sb.toString(); - } - - private String getQueryString(HttpServletRequest request) { - if (request.getQueryString() != null) { - return request.getQueryString().replace("&", "\\&"); - } - if (isGetRequest(request)) { - return toQueryString(request.getParameterMap()); - } - return null; - } - - private static String toQueryString(Map map) { - StringBuilder sb = new StringBuilder(); - for (Map.Entry entry : map.entrySet()) { - for (String value : entry.getValue()) { - if (sb.length() > 0) { - sb.append("\\&"); - } - sb.append(urlEncodeUTF8(entry.getKey())).append('=') - .append(urlEncodeUTF8(value)); - } - } - return sb.toString(); - } - - private static String urlEncodeUTF8(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException("Unable to URL encode " + s - + " using UTF-8", ex); - } - } - - private String getContent(MockHttpServletRequest request) throws IOException { - StringWriter bodyWriter = new StringWriter(); - FileCopyUtils.copy(request.getReader(), bodyWriter); - return bodyWriter.toString(); - } - } - - private static final class CurlResponseDocumentationAction implements - DocumentationAction { - - private final DocumentationWriter writer; - - private final MvcResult result; - - private final CurlConfiguration curlConfiguration; - - CurlResponseDocumentationAction(DocumentationWriter writer, MvcResult result, - CurlConfiguration curlConfiguration) { - this.writer = writer; - this.result = result; - this.curlConfiguration = curlConfiguration; - } - - @Override - public void perform() throws IOException { - if (this.curlConfiguration.isIncludeResponseHeaders()) { - HttpStatus status = HttpStatus.valueOf(this.result.getResponse() - .getStatus()); - this.writer.println(String.format("HTTP/1.1 %d %s", status.value(), - status.getReasonPhrase())); - for (String headerName : this.result.getResponse().getHeaderNames()) { - for (String header : this.result.getResponse().getHeaders(headerName)) { - this.writer.println(String.format("%s: %s", headerName, header)); - } - } - this.writer.println(); - } - this.writer.println(this.result.getResponse().getContentAsString()); + private boolean isNonStandardPort(DocumentableHttpServletRequest request) { + return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP) + || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java deleted file mode 100644 index b4ba9bc4..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlSnippetResultHandler.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2014-2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.curl; - -import org.springframework.restdocs.snippet.SnippetWritingResultHandler; -import org.springframework.test.web.servlet.ResultHandler; - -/** - * Abstract base class for Spring Mock MVC {@link ResultHandler ResultHandlers} that - * produce documentation snippets relating to cURL. - * - * @author Andy Wilkinson - */ -public abstract class CurlSnippetResultHandler extends SnippetWritingResultHandler { - - private final CurlConfiguration curlConfiguration = new CurlConfiguration(); - - public CurlSnippetResultHandler(String outputDir, String fileName) { - super(outputDir, fileName); - } - - CurlConfiguration getCurlConfiguration() { - return this.curlConfiguration; - } - - /** - * Specify whether or not the generated cURL snippets should have contents as if cURL - * had been invoked with {@code -i, --include}. - * - * @param include {@code true} to use {@code -i, --include}, otherwise false - * @return {@code this} - */ - public CurlSnippetResultHandler includeResponseHeaders(boolean include) { - this.curlConfiguration.setIncludeResponseHeaders(include); - return this; - } -} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java new file mode 100644 index 00000000..d840f925 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -0,0 +1,155 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.http; + +import java.io.IOException; +import java.util.List; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.DocumentationWriter; +import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; +import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.util.DocumentableHttpServletRequest; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.StringUtils; + +/** + * Static factory methods for documenting a RESTful API's HTTP requests. + * + * @author Andy Wilkinson + */ +public abstract class HttpDocumentation { + + private HttpDocumentation() { + + } + + /** + * Produces a documentation snippet containing the request formatted as an HTTP + * request + * + * @param outputDir The directory to which snippet should be written + * @return the handler that will produce the snippet + */ + public static SnippetWritingResultHandler documentHttpRequest(String outputDir) { + return new SnippetWritingResultHandler(outputDir, "http-request") { + + @Override + public void handle(MvcResult result, DocumentationWriter writer) + throws IOException { + writer.codeBlock("http", new HttpRequestDocumentationAction(writer, + result)); + } + }; + } + + /** + * Produces a documentation snippet containing the response formatted as the HTTP + * response sent by the server + * + * @param outputDir The directory to which snippet should be written + * @return the handler that will produce the snippet + */ + public static SnippetWritingResultHandler documentHttpResponse(String outputDir) { + return new SnippetWritingResultHandler(outputDir, "http-response") { + + @Override + public void handle(MvcResult result, DocumentationWriter writer) + throws IOException { + writer.codeBlock("http", new HttpResponseDocumentationAction(writer, + result)); + } + }; + } + + private static class HttpRequestDocumentationAction implements DocumentationAction { + + private final DocumentationWriter writer; + + private final MvcResult result; + + HttpRequestDocumentationAction(DocumentationWriter writer, MvcResult result) { + this.writer = writer; + this.result = result; + } + + @Override + public void perform() throws IOException { + DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( + this.result.getRequest()); + this.writer.printf("%s %s HTTP/1.1%n", request.getMethod(), + request.getRequestUriWithQueryString()); + for (Entry> header : request.getHeaders().entrySet()) { + for (String value : header.getValue()) { + this.writer.printf("%s: %s%n", header.getKey(), value); + } + } + if (requiresFormEncodingContentType(request)) { + this.writer.printf("%s: %s%n", HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE); + } + this.writer.println(); + if (request.getContentLength() > 0) { + this.writer.println(request.getContentAsString()); + } + else if (request.isPostRequest()) { + String queryString = request.getParameterMapAsQueryString(); + if (StringUtils.hasText(queryString)) { + this.writer.println(queryString); + } + } + } + + private boolean requiresFormEncodingContentType( + DocumentableHttpServletRequest request) { + return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null + && request.isPostRequest() + && StringUtils.hasText(request.getParameterMapAsQueryString()); + } + } + + private static final class HttpResponseDocumentationAction implements + DocumentationAction { + + private final DocumentationWriter writer; + + private final MvcResult result; + + HttpResponseDocumentationAction(DocumentationWriter writer, MvcResult result) { + this.writer = writer; + this.result = result; + } + + @Override + public void perform() throws IOException { + HttpStatus status = HttpStatus.valueOf(this.result.getResponse().getStatus()); + this.writer.println(String.format("HTTP/1.1 %d %s", status.value(), + status.getReasonPhrase())); + for (String headerName : this.result.getResponse().getHeaderNames()) { + for (String header : this.result.getResponse().getHeaders(headerName)) { + this.writer.println(String.format("%s: %s", headerName, header)); + } + } + this.writer.println(); + this.writer.println(this.result.getResponse().getContentAsString()); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java new file mode 100644 index 00000000..03fa98a4 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/util/DocumentableHttpServletRequest.java @@ -0,0 +1,223 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.util; + +import static org.springframework.restdocs.util.IterableEnumeration.iterable; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * An {@link HttpServletRequest} wrapper that provides a limited set of methods intended + * to help in the documentation of the request. + * + * @author Andy Wilkinson + * + */ +public class DocumentableHttpServletRequest { + + private final MockHttpServletRequest delegate; + + /** + * Creates a new {@link DocumentableHttpServletRequest} to document the given + * {@code request}. + * + * @param request the request that is to be documented + */ + public DocumentableHttpServletRequest(MockHttpServletRequest request) { + this.delegate = request; + } + + /** + * Whether or not this request is a {@code GET} request. + * + * @return {@code true} if it is a {@code GET} request, otherwise {@code false} + * @see HttpServletRequest#getMethod() + */ + public boolean isGetRequest() { + return RequestMethod.GET == RequestMethod.valueOf(this.delegate.getMethod()); + } + + /** + * Whether or not this request is a {@code POST} request. + * + * @return {@code true} if it is a {@code POST} request, otherwise {@code false} + * @see HttpServletRequest#getMethod() + */ + public boolean isPostRequest() { + return RequestMethod.POST == RequestMethod.valueOf(this.delegate.getMethod()); + } + + /** + * Returns a Map of the request's headers. The entries are ordered based on the + * ordering of {@link HttpServletRequest#getHeaderNames()} and + * {@link HttpServletRequest#getHeaders(String)}. + * + * @return the request's headers, keyed by name + * @see HttpServletRequest#getHeaderNames() + * @see HttpServletRequest#getHeaders(String) + */ + public Map> getHeaders() { + Map> headersByName = new LinkedHashMap>(); + for (String headerName : iterable(this.delegate.getHeaderNames())) { + List headers = new ArrayList(); + headersByName.put(headerName, headers); + for (String header : iterable(this.delegate.getHeaders(headerName))) { + headers.add(header); + } + } + return headersByName; + } + + /** + * Returns the request's scheme. + * + * @return the request's scheme + * @see HttpServletRequest#getScheme() + */ + public String getScheme() { + return this.delegate.getScheme(); + } + + /** + * Returns the name of the host to which the request was sent. + * + * @return the host's name + * @see HttpServletRequest#getServerName() + */ + public String getHost() { + return this.delegate.getServerName(); + } + + /** + * Returns the port to which the request was sent. + * + * @return the port + * @see HttpServletRequest#getServerPort() + */ + public int getPort() { + return this.delegate.getServerPort(); + } + + /** + * Returns the request's method. + * + * @return the request's method + * @see HttpServletRequest#getMethod() + */ + public String getMethod() { + return this.delegate.getMethod(); + } + + /** + * Returns the length of the request's content + * + * @return the content length + * @see HttpServletRequest#getContentLength() + */ + public long getContentLength() { + return this.delegate.getContentLengthLong(); + } + + /** + * Returns a {@code String} of the request's content + * + * @return the request's content + * @throws IOException if the content cannot be read + */ + public String getContentAsString() throws IOException { + StringWriter bodyWriter = new StringWriter(); + FileCopyUtils.copy(this.delegate.getReader(), bodyWriter); + return bodyWriter.toString(); + } + + /** + * Returns the request's URI including its query string. The query string is + * determined by calling {@link HttpServletRequest#getQueryString()}. If it's + * {@code null} and it is a {@code GET} request, the query string is then constructed + * from the request's {@link HttpServletRequest#getParameterMap()} parameter map. + * + * @return the URI of the request, including its query string + */ + public String getRequestUriWithQueryString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.delegate.getRequestURI()); + String queryString = getQueryString(); + if (StringUtils.hasText(queryString)) { + sb.append('?').append(queryString); + } + return sb.toString(); + } + + /** + * Returns the request's parameter map formatted as a query string + * + * @return The query string derived from the request's parameter map + * @see HttpServletRequest#getParameterMap() + */ + public String getParameterMapAsQueryString() { + return toQueryString(this.delegate.getParameterMap()); + } + + private String getQueryString() { + if (this.delegate.getQueryString() != null) { + return this.delegate.getQueryString(); + } + if (isGetRequest()) { + return getParameterMapAsQueryString(); + } + return null; + } + + private static String toQueryString(Map map) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + for (String value : entry.getValue()) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(urlEncodeUTF8(entry.getKey())).append('=') + .append(urlEncodeUTF8(value)); + } + } + return sb.toString(); + } + + private static String urlEncodeUTF8(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Unable to URL encode " + s + " using UTF-8", + ex); + } + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index 549b8fc9..b52a6ff1 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -84,7 +84,8 @@ public class RestDocumentationIntegrationTests { mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("basic")); assertExpectedSnippetFilesExist(new File("build/generated-snippets/basic"), - "request.asciidoc", "response.asciidoc", "request-response.asciidoc"); + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); } @Test @@ -96,7 +97,8 @@ public class RestDocumentationIntegrationTests { .andExpect(status().isOk()).andDo(document("{method-name}")); assertExpectedSnippetFilesExist(new File( "build/generated-snippets/parameterized-output-directory"), - "request.asciidoc", "response.asciidoc", "request-response.asciidoc"); + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); } @Test @@ -108,20 +110,23 @@ public class RestDocumentationIntegrationTests { mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( status().isOk()); assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-1/"), "request.asciidoc", - "response.asciidoc", "request-response.asciidoc"); + new File("build/generated-snippets/multi-step-1/"), + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( status().isOk()); assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-2/"), "request.asciidoc", - "response.asciidoc", "request-response.asciidoc"); + new File("build/generated-snippets/multi-step-2/"), + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( status().isOk()); assertExpectedSnippetFilesExist( - new File("build/generated-snippets/multi-step-3/"), "request.asciidoc", - "response.asciidoc", "request-response.asciidoc"); + new File("build/generated-snippets/multi-step-3/"), + "http-request.asciidoc", "http-response.asciidoc", + "curl-request.asciidoc"); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 24b76da9..f5c910a3 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -16,7 +16,11 @@ package org.springframework.restdocs.config; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import java.net.URI; @@ -72,6 +76,25 @@ public class RestDocumentationConfigurerTests { assertUriConfiguration("http", "localhost", 8081); } + @Test + public void noContentLengthHeaderWhenRequestHasNotContent() { + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort( + 8081).beforeMockMvcCreated(null, null); + postProcessor.postProcessRequest(this.request); + assertThat(this.request.getHeader("Content-Length"), is(nullValue())); + } + + @Test + public void contentLengthHeaderIsSetWhenRequestHasContent() { + RequestPostProcessor postProcessor = new RestDocumentationConfigurer().withPort( + 8081).beforeMockMvcCreated(null, null); + byte[] content = "Hello, world".getBytes(); + this.request.setContent(content); + postProcessor.postProcessRequest(this.request); + assertThat(this.request.getHeader("Content-Length"), + is(equalTo(Integer.toString(content.length)))); + } + private void assertUriConfiguration(String scheme, String host, int port) { assertEquals(scheme, this.request.getScheme()); assertEquals(host, this.request.getServerName()); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java index 7303adce..24131f71 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/curl/CurlDocumentationTests.java @@ -17,12 +17,8 @@ package org.springframework.restdocs.curl; import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.hasItems; -import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertThat; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse; -import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse; import java.io.BufferedReader; import java.io.File; @@ -34,10 +30,8 @@ import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.StubMvcResult; /** @@ -77,15 +71,6 @@ public class CurlDocumentationTests { hasItem("$ curl http://localhost/foo -i -X POST")); } - @Test - public void requestWithoutResponseHeaderInclusion() throws IOException { - documentCurlRequest("request-without-response-header-inclusion") - .includeResponseHeaders(false) - .handle(new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), null)); - assertThat(requestSnippetLines("request-without-response-header-inclusion"), - hasItem("$ curl http://localhost/foo")); - } - @Test public void requestWithContent() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); @@ -192,67 +177,6 @@ public class CurlDocumentationTests { hasItem("$ curl http://localhost/foo -i -H \"Content-Type: application/json\" -H \"a: alpha\"")); } - @Test - public void basicResponse() throws IOException { - documentCurlResponse("basic-response").handle( - new StubMvcResult(null, new MockHttpServletResponse())); - assertThat(responseSnippetLines("basic-response"), hasItem("HTTP/1.1 200 OK")); - } - - @Test - public void nonOkResponse() throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setStatus(HttpStatus.BAD_REQUEST.value()); - documentCurlResponse("non-ok-response").handle(new StubMvcResult(null, response)); - assertThat(responseSnippetLines("non-ok-response"), - hasItem("HTTP/1.1 400 Bad Request")); - } - - @Test - public void responseWithHeaders() throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setHeader("a", "alpha"); - documentCurlResponse("non-ok-response").handle(new StubMvcResult(null, response)); - assertThat(responseSnippetLines("non-ok-response"), - hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", "a: alpha")); - } - - @Test - public void responseWithHeaderInclusionDisabled() throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setHeader("a", "alpha"); - response.getWriter().append("content"); - documentCurlResponse("response-with-header-inclusion-disabled") - .includeResponseHeaders(false).handle(new StubMvcResult(null, response)); - List responseSnippetLines = responseSnippetLines("response-with-header-inclusion-disabled"); - assertThat( - responseSnippetLines, - not(hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", - "a: alpha"))); - assertThat(responseSnippetLines, hasItem("content")); - } - - @Test - public void responseWithContent() throws IOException { - MockHttpServletResponse response = new MockHttpServletResponse(); - response.getWriter().append("content"); - documentCurlResponse("response-with-content").handle( - new StubMvcResult(null, response)); - assertThat(responseSnippetLines("response-with-content"), - hasItems("HTTP/1.1 200 OK", "content")); - } - - @Test - public void requestAndResponse() throws IOException { - documentCurlRequestAndResponse("request-and-response").handle( - new StubMvcResult(new MockHttpServletRequest("GET", "/foo"), - new MockHttpServletResponse())); - assertThat(requestResponseSnippetLines("request-and-response"), - hasItems("$ curl http://localhost/foo -i", "HTTP/1.1 200 OK")); - } - @Test public void httpWithNonStandardPort() throws IOException { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); @@ -296,16 +220,7 @@ public class CurlDocumentationTests { } private List requestSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "request"); - } - - private List responseSnippetLines(String snippetName) throws IOException { - return snippetLines(snippetName, "response"); - } - - private List requestResponseSnippetLines(String snippetName) - throws IOException { - return snippetLines(snippetName, "request-response"); + return snippetLines(snippetName, "curl-request"); } private List snippetLines(String snippetName, String snippetType) diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java new file mode 100644 index 00000000..1d254658 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/http/HttpDocumentationTests.java @@ -0,0 +1,175 @@ +/* + * Copyright 2014-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.http; + +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest; +import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.StubMvcResult; + +/** + * Tests for {@link HttpDocumentation} + * + * @author Andy Wilkinson + */ +public class HttpDocumentationTests { + + private final File outputDir = new File("build/http-documentation-tests"); + + @Before + public void setup() { + System.setProperty("org.springframework.restdocs.outputDir", + this.outputDir.getAbsolutePath()); + } + + @After + public void cleanup() { + System.clearProperty("org.springframework.restdocs.outputDir"); + } + + @Test + public void getRequest() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addHeader("Alpha", "a"); + documentHttpRequest("get-request").handle(new StubMvcResult(request, null)); + assertThat(requestSnippetLines("get-request"), + hasItems("GET /foo HTTP/1.1", "Alpha: a")); + } + + @Test + public void getRequestWithQueryString() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo?bar=baz"); + documentHttpRequest("get-request-with-query-string").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("get-request-with-query-string"), + hasItems("GET /foo?bar=baz HTTP/1.1")); + } + + @Test + public void getRequestWithParameter() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); + request.addParameter("b&r", "baz"); + documentHttpRequest("get-request-with-parameter").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("get-request-with-parameter"), + hasItems("GET /foo?b%26r=baz HTTP/1.1")); + } + + @Test + public void postRequestWithContent() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); + byte[] content = "Hello, world".getBytes(); + request.setContent(content); + documentHttpRequest("post-request-with-content").handle( + new StubMvcResult(request, null)); + assertThat(requestSnippetLines("post-request-with-content"), + hasItems("POST /foo HTTP/1.1", "Hello, world")); + } + + @Test + public void postRequestWithParameter() throws IOException { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/foo"); + request.addParameter("b&r", "baz"); + request.addParameter("a", "alpha"); + documentHttpRequest("post-request-with-parameter").handle( + new StubMvcResult(request, null)); + assertThat( + requestSnippetLines("post-request-with-parameter"), + hasItems("POST /foo HTTP/1.1", + "Content-Type: application/x-www-form-urlencoded", + "b%26r=baz&a=alpha")); + } + + @Test + public void basicResponse() throws IOException { + documentHttpResponse("basic-response").handle( + new StubMvcResult(null, new MockHttpServletResponse())); + assertThat(responseSnippetLines("basic-response"), hasItem("HTTP/1.1 200 OK")); + } + + @Test + public void nonOkResponse() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(HttpStatus.BAD_REQUEST.value()); + documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response)); + assertThat(responseSnippetLines("non-ok-response"), + hasItem("HTTP/1.1 400 Bad Request")); + } + + @Test + public void responseWithHeaders() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setHeader("a", "alpha"); + documentHttpResponse("non-ok-response").handle(new StubMvcResult(null, response)); + assertThat(responseSnippetLines("non-ok-response"), + hasItems("HTTP/1.1 200 OK", "Content-Type: application/json", "a: alpha")); + } + + @Test + public void responseWithContent() throws IOException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().append("content"); + documentHttpResponse("response-with-content").handle( + new StubMvcResult(null, response)); + assertThat(responseSnippetLines("response-with-content"), + hasItems("HTTP/1.1 200 OK", "content")); + } + + private List requestSnippetLines(String snippetName) throws IOException { + return snippetLines(snippetName, "http-request"); + } + + private List responseSnippetLines(String snippetName) throws IOException { + return snippetLines(snippetName, "http-response"); + } + + private List snippetLines(String snippetName, String snippetType) + throws IOException { + File snippetDir = new File(this.outputDir, snippetName); + File snippetFile = new File(snippetDir, snippetType + ".asciidoc"); + String line = null; + List lines = new ArrayList(); + BufferedReader reader = new BufferedReader(new FileReader(snippetFile)); + try { + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + finally { + reader.close(); + } + return lines; + } +}