Add support for generating an HTTP request documentation snippet

Closes gh-13
This commit is contained in:
Andy Wilkinson
2015-03-25 13:49:44 +00:00
parent 86b431018d
commit e00734d693
16 changed files with 725 additions and 445 deletions

View File

@@ -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 `index` in the project's `build/generated-snippets/` directory. Three files will
be written: be written:
- `index/request.asciidoc` - `index/curl-request.asciidoc`
- `index/response.asciidoc` - `index/http-request.asciidoc`
- `index/request-response.asciidoc` - `index/http-response.asciidoc`
#### Parameterized output directories #### Parameterized output directories
@@ -340,8 +340,8 @@ which the snippets are written. For example, to include both the request and res
snippets described above: snippets described above:
``` ```
include::{generated}/index/request.asciidoc[] include::{generated}/index/curl-request.asciidoc[]
include::{generated}/index/response.asciidoc[] include::{generated}/index/http-response.asciidoc[]
``` ```
## Generating snippets in your IDE ## Generating snippets in your IDE

View File

@@ -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 For example, a request that attempts to apply a non-existent tag to a note will produce a
`400 Bad Request` response: `400 Bad Request` response:
include::{generated}/error-example/response.asciidoc[] include::{generated}/error-example/http-response.asciidoc[]
[[overview-hypermedia]] [[overview-hypermedia]]
== Hypermedia == Hypermedia
@@ -103,7 +103,7 @@ include::{generated}/index-example/response-fields.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/notes-list-example/request.asciidoc[] include::{generated}/notes-list-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/notes-create-example/request.asciidoc[] include::{generated}/notes-create-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/tags-list-example/request.asciidoc[] include::{generated}/tags-list-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/tags-create-example/request.asciidoc[] include::{generated}/tags-create-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/note-get-example/request.asciidoc[] include::{generated}/note-get-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/note-update-example/request.asciidoc[] include::{generated}/note-update-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/tag-get-example/request.asciidoc[] include::{generated}/tag-get-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/tag-update-example/request.asciidoc[] include::{generated}/tag-update-example/curl-request.asciidoc[]
==== Example response ==== Example response
include::{generated}/tag-update-example/response.asciidoc[] include::{generated}/tag-update-example/http-response.asciidoc[]

View File

@@ -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 You can check that the service is up and running by executing a simple request using
cURL: cURL:
include::{generated}/index/1/request.asciidoc[] include::{generated}/index/1/curl-request.asciidoc[]
This request should yield the following response in the This request should yield the following response in the
http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format: 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. 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 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: 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 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: 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 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: `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, 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: 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: 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. 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 Referring back to the response for the service's index, the URI for working with tags is
include as a link: 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 To create a tag you need to execute a `POST` request to this URI, including a JSON
payload containing the name of the tag: 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 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: `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 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: 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: 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 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 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: 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: 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 To verify that the tag has been associated with the note, we can perform a `GET` request
against the URI from the `tags` link: 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: 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 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: 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: 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: 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 We can use that link now and execute a `GET` request to see that the note now has a
single tag: 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[]

View File

@@ -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 For example, a request that attempts to apply a non-existent tag to a note will produce a
`400 Bad Request` response: `400 Bad Request` response:
include::{generated}/error-example/response.asciidoc[] include::{generated}/error-example/http-response.asciidoc[]
[[overview-hypermedia]] [[overview-hypermedia]]
== Hypermedia == Hypermedia
@@ -103,7 +103,7 @@ include::{generated}/index-example/response-fields.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/notes-list-example/request.asciidoc[] include::{generated}/notes-list-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/notes-create-example/request.asciidoc[] include::{generated}/notes-create-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/tags-list-example/request.asciidoc[] include::{generated}/tags-list-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/tags-create-example/request.asciidoc[] include::{generated}/tags-create-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/note-get-example/request.asciidoc[] include::{generated}/note-get-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/note-update-example/request.asciidoc[] include::{generated}/note-update-example/curl-request.asciidoc[]
==== Example response ==== Example response
include::{generated}/note-update-example/response.asciidoc[] include::{generated}/note-update-example/http-response.asciidoc[]
[[resources-tag]] [[resources-tag]]
@@ -282,11 +282,11 @@ include::{generated}/tag-get-example/response-fields.asciidoc[]
==== Example request ==== Example request
include::{generated}/tag-get-example/request.asciidoc[] include::{generated}/tag-get-example/curl-request.asciidoc[]
==== Example response ==== 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 ==== Example request
include::{generated}/tag-update-example/request.asciidoc[] include::{generated}/tag-update-example/curl-request.asciidoc[]
==== Example response ==== Example response
include::{generated}/tag-update-example/response.asciidoc[] include::{generated}/tag-update-example/http-response.asciidoc[]

View File

@@ -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 You can check that the service is up and running by executing a simple request using
cURL: cURL:
include::{generated}/index/1/request.asciidoc[] include::{generated}/index/1/curl-request.asciidoc[]
This request should yield the following response: 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. 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 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: 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 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: 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 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: `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 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: 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: 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. 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 Referring back to the response for the service's index, the URI for working with tags is
include as a link: 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 To create a tag you need to execute a `POST` request to this URI, including a JSON
payload containing the name of the tag: 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 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: `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 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: 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: 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 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: 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: 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: 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 To see the note's tags, execute a `GET` request against the URI of the note's
`note-tags` link: `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: 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 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: 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: 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: 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 We can use that link now and execute a `GET` request to see that the note now has a
single tag: 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[]

View File

@@ -17,8 +17,8 @@
package org.springframework.restdocs; package org.springframework.restdocs;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse; import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields;
@@ -53,8 +53,8 @@ public class RestDocumentationResultHandler implements ResultHandler {
this.delegates = new ArrayList<ResultHandler>(); this.delegates = new ArrayList<ResultHandler>();
this.delegates.add(documentCurlRequest(this.outputDir)); this.delegates.add(documentCurlRequest(this.outputDir));
this.delegates.add(documentCurlResponse(this.outputDir)); this.delegates.add(documentHttpRequest(this.outputDir));
this.delegates.add(documentCurlRequestAndResponse(this.outputDir)); this.delegates.add(documentHttpResponse(this.outputDir));
} }
@Override @Override

View File

@@ -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.ConfigurableMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter;
import org.springframework.util.StringUtils;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
/** /**
@@ -108,8 +109,19 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter {
request.setScheme(RestDocumentationConfigurer.this.scheme); request.setScheme(RestDocumentationConfigurer.this.scheme);
request.setServerPort(RestDocumentationConfigurer.this.port); request.setServerPort(RestDocumentationConfigurer.this.port);
request.setServerName(RestDocumentationConfigurer.this.host); request.setServerName(RestDocumentationConfigurer.this.host);
configureContentLengthHeaderIfAppropriate(request);
return 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());
}
}
}; };
} }

View File

@@ -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;
}
}

View File

@@ -16,29 +16,21 @@
package org.springframework.restdocs.curl; package org.springframework.restdocs.curl;
import static org.springframework.restdocs.util.IterableEnumeration.iterable;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.util.List;
import java.io.UnsupportedEncodingException; import java.util.Map.Entry;
import java.net.URLEncoder;
import java.util.Map;
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;
import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; 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.test.web.servlet.MvcResult;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils; 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 * Static factory methods for documenting a RESTful API as if it were being driven using
* the cURL command-line utility. * the cURL command-line utility.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Yann Le Guern * @author Yann Le Guern
*/ */
@@ -50,58 +42,17 @@ public abstract class CurlDocumentation {
/** /**
* Produces a documentation snippet containing the request formatted as a cURL command * Produces a documentation snippet containing the request formatted as a cURL command
* *
* @param outputDir The directory to which snippet should be written * @param outputDir The directory to which snippet should be written
* @return the handler that will produce the snippet * @return the handler that will produce the snippet
*/ */
public static CurlSnippetResultHandler documentCurlRequest(String outputDir) { public static SnippetWritingResultHandler documentCurlRequest(String outputDir) {
return new CurlSnippetResultHandler(outputDir, "request") { return new SnippetWritingResultHandler(outputDir, "curl-request") {
@Override @Override
public void handle(MvcResult result, DocumentationWriter writer) public void handle(MvcResult result, DocumentationWriter writer)
throws IOException { throws IOException {
writer.shellCommand(new CurlRequestDocumentationAction(writer, result, 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()));
} }
}; };
} }
@@ -121,152 +72,55 @@ public abstract class CurlDocumentation {
private final MvcResult result; private final MvcResult result;
private final CurlConfiguration curlConfiguration; CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result) {
CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result,
CurlConfiguration curlConfiguration) {
this.writer = writer; this.writer = writer;
this.result = result; this.result = result;
this.curlConfiguration = curlConfiguration;
} }
@Override @Override
public void perform() throws IOException { 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(), this.writer.print(String.format("curl %s://%s", request.getScheme(),
request.getServerName())); request.getHost()));
if (isNonStandardPort(request)) { 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())); this.writer.print(String.format(" -X %s", request.getMethod()));
} }
for (String headerName : iterable(request.getHeaderNames())) { for (Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
for (String header : iterable(request.getHeaders(headerName))) { for (String header : entry.getValue()) {
this.writer this.writer.print(String.format(" -H \"%s: %s\"", entry.getKey(),
.print(String.format(" -H \"%s: %s\"", headerName, header)); header));
} }
} }
if (request.getContentLengthLong() > 0) { if (request.getContentLength() > 0) {
this.writer.print(String.format(" -d '%s'", getContent(request))); this.writer
.print(String.format(" -d '%s'", request.getContentAsString()));
} }
else if (isPostRequest(request)) { else if (request.isPostRequest()) {
Map<String, String[]> parameters = request.getParameterMap(); String queryString = request.getParameterMapAsQueryString();
if (!parameters.isEmpty()) { if (StringUtils.hasText(queryString)) {
this.writer.print(String this.writer.print(String.format(" -d '%s'",
.format(" -d '%s'", toQueryString(parameters))); queryString.replace("&", "\\&")));
} }
} }
this.writer.println(); this.writer.println();
} }
private boolean isGetRequest(HttpServletRequest request) { private boolean isNonStandardPort(DocumentableHttpServletRequest request) {
return RequestMethod.GET == RequestMethod.valueOf(request.getMethod()); return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP)
} || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS);
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<String, String[]> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String[]> 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());
} }
} }

View File

@@ -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;
}
}

View File

@@ -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<String, List<String>> 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());
}
}
}

View File

@@ -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<String, List<String>> getHeaders() {
Map<String, List<String>> headersByName = new LinkedHashMap<String, List<String>>();
for (String headerName : iterable(this.delegate.getHeaderNames())) {
List<String> headers = new ArrayList<String>();
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<String, String[]> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String[]> 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);
}
}
}

View File

@@ -84,7 +84,8 @@ public class RestDocumentationIntegrationTests {
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andDo(document("basic")); .andExpect(status().isOk()).andDo(document("basic"));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/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 @Test
@@ -96,7 +97,8 @@ public class RestDocumentationIntegrationTests {
.andExpect(status().isOk()).andDo(document("{method-name}")); .andExpect(status().isOk()).andDo(document("{method-name}"));
assertExpectedSnippetFilesExist(new File( assertExpectedSnippetFilesExist(new File(
"build/generated-snippets/parameterized-output-directory"), "build/generated-snippets/parameterized-output-directory"),
"request.asciidoc", "response.asciidoc", "request-response.asciidoc"); "http-request.asciidoc", "http-response.asciidoc",
"curl-request.asciidoc");
} }
@Test @Test
@@ -108,20 +110,23 @@ public class RestDocumentationIntegrationTests {
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(
status().isOk()); status().isOk());
assertExpectedSnippetFilesExist( assertExpectedSnippetFilesExist(
new File("build/generated-snippets/multi-step-1/"), "request.asciidoc", new File("build/generated-snippets/multi-step-1/"),
"response.asciidoc", "request-response.asciidoc"); "http-request.asciidoc", "http-response.asciidoc",
"curl-request.asciidoc");
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(
status().isOk()); status().isOk());
assertExpectedSnippetFilesExist( assertExpectedSnippetFilesExist(
new File("build/generated-snippets/multi-step-2/"), "request.asciidoc", new File("build/generated-snippets/multi-step-2/"),
"response.asciidoc", "request-response.asciidoc"); "http-request.asciidoc", "http-response.asciidoc",
"curl-request.asciidoc");
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect(
status().isOk()); status().isOk());
assertExpectedSnippetFilesExist( assertExpectedSnippetFilesExist(
new File("build/generated-snippets/multi-step-3/"), "request.asciidoc", new File("build/generated-snippets/multi-step-3/"),
"response.asciidoc", "request-response.asciidoc"); "http-request.asciidoc", "http-response.asciidoc",
"curl-request.asciidoc");
} }

View File

@@ -16,7 +16,11 @@
package org.springframework.restdocs.config; 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.assertEquals;
import static org.junit.Assert.assertThat;
import java.net.URI; import java.net.URI;
@@ -72,6 +76,25 @@ public class RestDocumentationConfigurerTests {
assertUriConfiguration("http", "localhost", 8081); 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) { private void assertUriConfiguration(String scheme, String host, int port) {
assertEquals(scheme, this.request.getScheme()); assertEquals(scheme, this.request.getScheme());
assertEquals(host, this.request.getServerName()); assertEquals(host, this.request.getServerName());

View File

@@ -17,12 +17,8 @@
package org.springframework.restdocs.curl; package org.springframework.restdocs.curl;
import static org.hamcrest.CoreMatchers.hasItem; 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.junit.Assert.assertThat;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest; 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.BufferedReader;
import java.io.File; import java.io.File;
@@ -34,10 +30,8 @@ import java.util.List;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.StubMvcResult; import org.springframework.restdocs.StubMvcResult;
/** /**
@@ -77,15 +71,6 @@ public class CurlDocumentationTests {
hasItem("$ curl http://localhost/foo -i -X POST")); 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 @Test
public void requestWithContent() throws IOException { public void requestWithContent() throws IOException {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); 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\"")); 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<String> 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 @Test
public void httpWithNonStandardPort() throws IOException { public void httpWithNonStandardPort() throws IOException {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
@@ -296,16 +220,7 @@ public class CurlDocumentationTests {
} }
private List<String> requestSnippetLines(String snippetName) throws IOException { private List<String> requestSnippetLines(String snippetName) throws IOException {
return snippetLines(snippetName, "request"); return snippetLines(snippetName, "curl-request");
}
private List<String> responseSnippetLines(String snippetName) throws IOException {
return snippetLines(snippetName, "response");
}
private List<String> requestResponseSnippetLines(String snippetName)
throws IOException {
return snippetLines(snippetName, "request-response");
} }
private List<String> snippetLines(String snippetName, String snippetType) private List<String> snippetLines(String snippetName, String snippetType)

View File

@@ -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<String> requestSnippetLines(String snippetName) throws IOException {
return snippetLines(snippetName, "http-request");
}
private List<String> responseSnippetLines(String snippetName) throws IOException {
return snippetLines(snippetName, "http-response");
}
private List<String> 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<String> lines = new ArrayList<String>();
BufferedReader reader = new BufferedReader(new FileReader(snippetFile));
try {
while ((line = reader.readLine()) != null) {
lines.add(line);
}
}
finally {
reader.close();
}
return lines;
}
}