From 01e37f1e4fd1e4c9f2c018344a2224dc4b40ccca Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Oct 2014 14:57:34 +0000 Subject: [PATCH] =?UTF-8?q?Remove=20CGLib=20proxy=20=E2=80=9Cmagic?= =?UTF-8?q?=E2=80=9D=20in=20favour=20of=20an=20explicit=20document=20metho?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the name of an output file was automatically determined by the name of the method from which the mockMvc.perform call was made. While (somewhat) clever, to support this for non @Test methods, this required the use of a CGLib proxy to push and pop some context that kept track of the name of the current method. This meant it only worked for non-private methods. It also made it hard to look at the code and see what would and would not be documented. This commit updates the library to provide an explicit document method instead. This method takes the path of an output directory and a MockMvc ResultActions instance, typically returned from a call to mockMvc.perform. For example: document("index", this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) .andExpect(status().isOk())); This will perform a GET request to "/", assert that the response is 200 OK and write documentation snippets for the request and response to a directory named index. --- README.md | 64 ++++------ .../src/documentation/asciidoc/main.asciidoc | 42 +++--- .../notes/GettingStartedDocumentation.java | 114 ++++++++++------- .../src/documentation/asciidoc/main.asciidoc | 44 +++---- .../notes/GettingStartedDocumentation.java | 120 +++++++++++------- .../restdocs/core/DocumentationContext.java | 62 --------- .../restdocs/core/RestDocumentation.java | 34 +++++ .../core/RestDocumentationConfiguration.java | 15 +-- .../RestDocumentationJUnit4ClassRunner.java | 70 ---------- .../core/RestDocumentationResultHandlers.java | 49 +++---- 10 files changed, 262 insertions(+), 352 deletions(-) delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationContext.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationJUnit4ClassRunner.java diff --git a/README.md b/README.md index fcf494a7..28035bce 100644 --- a/README.md +++ b/README.md @@ -66,54 +66,46 @@ apply plugin: 'org.springframework.restdocs' ### Programatically generated snippets Spring's MVC Test framework is used to make requests to the service that you are -documenting. Through the use of a custom JUnit runner and some MockMvc configuration -documentation snippets for those request and their responses is automatically generated. +documenting. Any such request wrapped in a call to `RestDocumentation.document` will +produce individual documentation snippets for its request and its response as well as +a snippet that contains both its request and its response. -The runner is configured using `@RunWith` on the documentation class. For example: - -```java -@RunWith(RestDocumentationJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = Application.class) -@WebAppConfiguration -public class GettingStartedDocumentation { - // … -} -``` - -`RestDocumentationJUnit4ClassRunner` is an extension of Spring Framework's -`SpringJUnit4ClassRunner` so all the standard Spring Test Framework functionality is -available. - -The MockMvc configuration is applied during its creation. This is typically done in -an `@Before` method, for example: +You can configure the scheme, host, and port of any URIs that appear in the +documentation snippets: ```java @Before -public void setUp() { - this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(new RestDocumentationConfiguration()).build(); -} + public void setUp() { + this.mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(new RestDocumentationConfiguration() + .withScheme("https") + .withHost("localhost") + .withPort(8443)) + .build(); + } ``` -With this configuration in place any requests made to the REST service using MockMvc -will have their requests and responses documented. For example: +The default values are `http`, `localhost`, and `8080`. You can omit the above +configuration if these defaults meet your needs. + +To document a MockMvc call, wrap it in a call to `RestDocumentation.document`: ```java public void getIndex() { - this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)); + document("index", this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))); } ``` The code above will perform a `GET` request against the index (`/`) of the service with -an accept header indicating that a JSON response is required. It will automatically -write the cURL command for the request and the resulting response to files beneath the -project's `build/generated-documentation` directory. This location is automatically -configured by the Gradle plugin. The names of the files are determined by the name of -the class and method from which the call was made. In this example, the files will be -called: +an accept header indicating that a JSON response is required. It will write the cURL +command for the request and the resulting response to files in a directory named +`index` in the project's `build/generated-documentation/` directory. Three files will +be written: - - `GettingStartedDocumentation/getIndexRequest.asciidoc` - - `GettingStartedDocumentation/getIndexResponse.asciidoc` + - `index/request.asciidoc` + - `index/response.asciidoc` + - `index/request-response.asciidoc` ### Documentation written in Asciidoc @@ -129,8 +121,8 @@ that you can use to reference the directory to which the snippets are written. F example, to include both the request and response snippets described above: ``` -include::{generated}/GettingStartedDocumentation/getIndexRequest.asciidoc[] -include::{generated}/GettingStartedDocumentation/getIndexResponse.asciidoc[] +include::{generated}/index/request.asciidoc[] +include::{generated}/index/response.asciidoc[] ``` ## Learning more diff --git a/rest-notes-spring-data-rest/src/documentation/asciidoc/main.asciidoc b/rest-notes-spring-data-rest/src/documentation/asciidoc/main.asciidoc index 8cb4fcc3..c766b93f 100644 --- a/rest-notes-spring-data-rest/src/documentation/asciidoc/main.asciidoc +++ b/rest-notes-spring-data-rest/src/documentation/asciidoc/main.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}/GettingStartedDocumentation/indexRequest.asciidoc[] +include::{generated}/index/request.asciidoc[] This request should yield the following response in the http://stateless.co/hal_specification.html[Hypertext Application Language (HAL)] format: -include::{generated}/GettingStartedDocumentation/indexResponse.asciidoc[] +include::{generated}/index/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}/GettingStartedDocumentation/indexResponse.asciidoc[] +include::{generated}/index/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}/GettingStartedDocumentation/createNoteRequest.asciidoc[] +include::{generated}/create-note/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}/GettingStartedDocumentation/createNoteResponse.asciidoc[] +include::{generated}/create-note/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}/GettingStartedDocumentation/getNoteRequest.asciidoc[] +include::{generated}/get-note/request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/GettingStartedDocumentation/getNoteResponse.asciidoc[] +include::{generated}/get-note/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}/GettingStartedDocumentation/indexResponse.asciidoc[] +include::{generated}/index/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}/GettingStartedDocumentation/createTagRequest.asciidoc[] +include::{generated}/create-tag/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}/GettingStartedDocumentation/createTagResponse.asciidoc[] +include::{generated}/create-tag/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}/GettingStartedDocumentation/getTagRequest.asciidoc[] +include::{generated}/get-tag/request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/GettingStartedDocumentation/getTagResponse.asciidoc[] +include::{generated}/get-tag/response.asciidoc[] @@ -132,24 +132,24 @@ 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}/GettingStartedDocumentation/createTaggedNoteRequest.asciidoc[] +include::{generated}/create-tagged-note/request.asciidoc[] Once again, the response's `Location` header tells us the URI of the newly created note: -include::{generated}/GettingStartedDocumentation/createTaggedNoteResponse.asciidoc[] +include::{generated}/create-tagged-note/response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/GettingStartedDocumentation/getTaggedNoteRequestResponse.asciidoc[] +include::{generated}/get-tagged-note/request-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}/GettingStartedDocumentation/getTagsRequest.asciidoc[] +include::{generated}/get-tags/request.asciidoc[] The response embeds information about the tag that we've just associated with the note: -include::{generated}/GettingStartedDocumentation/getTagsResponse.asciidoc[] +include::{generated}/get-tags/response.asciidoc[] @@ -159,17 +159,17 @@ 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}/GettingStartedDocumentation/tagExistingNoteRequest.asciidoc[] +include::{generated}/tag-existing-note/request.asciidoc[] This request should produce a `204 No Content` response: -include::{generated}/GettingStartedDocumentation/tagExistingNoteResponse.asciidoc[] +include::{generated}/tag-existing-note/response.asciidoc[] When we first created this note, we noted the tags link included in its details: -include::{generated}/GettingStartedDocumentation/createNoteResponse.asciidoc[] +include::{generated}/get-note/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}/GettingStartedDocumentation/getTagsForExistingNoteRequestResponse.asciidoc[] +include::{generated}/get-tags-for-existing-note/request-response.asciidoc[] diff --git a/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java b/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java index 079313d8..ae231f53 100644 --- a/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java +++ b/rest-notes-spring-data-rest/src/documentation/java/com/example/notes/GettingStartedDocumentation.java @@ -19,6 +19,7 @@ package com.example.notes; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.restdocs.core.RestDocumentation.document; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -38,7 +39,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.core.RestDocumentationConfiguration; -import org.springframework.restdocs.core.RestDocumentationJUnit4ClassRunner; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -49,7 +50,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; -@RunWith(RestDocumentationJUnit4ClassRunner.class) +@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class GettingStartedDocumentation { @@ -70,10 +71,12 @@ public class GettingStartedDocumentation { @Test public void index() throws Exception { - this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("_links.notes", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))); + document( + "index", + this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("_links.notes", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue())))); } @Test @@ -98,41 +101,49 @@ public class GettingStartedDocumentation { note.put("title", "Note creation with cURL"); note.put("body", "An example of how to create a note using cURL"); - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())).andReturn() - .getResponse().getHeader("Location"); + String noteLocation = document( + "create-note", + this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue()))) + .andReturn().getResponse().getHeader("Location"); return noteLocation; } void getNote(String noteLocation) throws Exception { - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))); + document( + "get-note", + this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue())))); } String createTag() throws Exception, JsonProcessingException { Map tag = new HashMap(); tag.put("name", "getting-started"); - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())).andReturn() - .getResponse().getHeader("Location"); + String tagLocation = document( + "create-tag", + this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue()))) + .andReturn().getResponse().getHeader("Location"); return tagLocation; } void getTag(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("_links.notes", is(notNullValue()))); + document( + "get-tag", + this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("name", is(notNullValue()))) + .andExpect(jsonPath("_links.notes", is(notNullValue())))); } String createTaggedNote(String tag) throws Exception { @@ -141,50 +152,59 @@ public class GettingStartedDocumentation { note.put("body", "An example of how to create a tagged note using cURL"); note.put("tags", Arrays.asList(tag)); - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())).andReturn() - .getResponse().getHeader("Location"); + String noteLocation = document( + "create-tagged-note", + this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue()))) + .andReturn().getResponse().getHeader("Location"); return noteLocation; } void getTaggedNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("_links.tags", is(notNullValue()))); + document( + "get-tagged-note", + this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("_links.tags", is(notNullValue())))); } void getTags(String taggedNoteLocation) throws Exception { String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) .andReturn(), "tags"); - this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))); + document("get-tags", + this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("_embedded.tags", hasSize(1)))); } void tagExistingNote(String noteLocation, String tagLocation) throws Exception { Map update = new HashMap(); update.put("tags", Arrays.asList(tagLocation)); - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(update))).andExpect( - status().isNoContent()); + document( + "tag-existing-note", + this.mockMvc.perform( + patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(update))).andExpect( + status().isNoContent())); } void getTaggedExistingNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()); + document("get-tagged-existing-note", this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk())); } void getTagsForExistingNote(String taggedNoteLocation) throws Exception { String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) .andReturn(), "tags"); - this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("_embedded.tags", hasSize(1))); + document("get-tags-for-existing-note", + this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("_embedded.tags", hasSize(1)))); } private String getLink(MvcResult result, String href) diff --git a/rest-notes-spring-hateoas/src/documentation/asciidoc/main.asciidoc b/rest-notes-spring-hateoas/src/documentation/asciidoc/main.asciidoc index 1f0be40d..a6ddf318 100644 --- a/rest-notes-spring-hateoas/src/documentation/asciidoc/main.asciidoc +++ b/rest-notes-spring-hateoas/src/documentation/asciidoc/main.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}/GettingStartedDocumentation/indexRequest.asciidoc[] +include::{generated}/index/request.asciidoc[] This request should yield the following response: -include::{generated}/GettingStartedDocumentation/indexResponse.asciidoc[] +include::{generated}/index/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}/GettingStartedDocumentation/indexResponse.asciidoc[] +include::{generated}/index/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}/GettingStartedDocumentation/createNoteRequest.asciidoc[] +include::{generated}/create-note/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}/GettingStartedDocumentation/createNoteResponse.asciidoc[] +include::{generated}/create-note/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}/GettingStartedDocumentation/getNoteRequest.asciidoc[] +include::{generated}/get-note/request.asciidoc[] This request will produce a response with the note's details in its body: -include::{generated}/GettingStartedDocumentation/getNoteResponse.asciidoc[] +include::{generated}/get-note/response.asciidoc[] Note the `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}/GettingStartedDocumentation/indexResponse.asciidoc[] +include::{generated}/index/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}/GettingStartedDocumentation/createTagRequest.asciidoc[] +include::{generated}/create-tag/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}/GettingStartedDocumentation/createTagResponse.asciidoc[] +include::{generated}/create-tag/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}/GettingStartedDocumentation/getTagRequest.asciidoc[] +include::{generated}/get-tag/request.asciidoc[] This request will produce a response with the tag's details in its body: -include::{generated}/GettingStartedDocumentation/getTagResponse.asciidoc[] +include::{generated}/get-tag/response.asciidoc[] @@ -131,23 +131,23 @@ 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}/GettingStartedDocumentation/createTaggedNoteRequest.asciidoc[] +include::{generated}/create-tagged-note/request.asciidoc[] Once again, the response's `Location` header tells use the URI of the newly created note: -include::{generated}/GettingStartedDocumentation/createTaggedNoteResponse.asciidoc[] +include::{generated}/create-tagged-note/response.asciidoc[] As before, a `GET` request executed against this URI will retrieve the note's details: -include::{generated}/GettingStartedDocumentation/getTaggedNoteRequestResponse.asciidoc[] +include::{generated}/get-tagged-note/request-response.asciidoc[] To see the note's tags, execute a `GET` request against the URI of the note's tags link: -include::{generated}/GettingStartedDocumentation/getTagsRequest.asciidoc[] +include::{generated}/get-tags/request.asciidoc[] The response shows that, as expected, the note has a single tag: -include::{generated}/GettingStartedDocumentation/getTagsResponse.asciidoc[] +include::{generated}/get-tags/response.asciidoc[] @@ -157,17 +157,17 @@ 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}/GettingStartedDocumentation/tagExistingNoteRequest.asciidoc[] +include::{generated}/tag-existing-note/request.asciidoc[] -This request should produce a `200 OK` response: +This request should produce a `204 No Content` response: -include::{generated}/GettingStartedDocumentation/tagExistingNoteResponse.asciidoc[] +include::{generated}/tag-existing-note/response.asciidoc[] When we first created this note, we noted the tags link included in its details: -include::{generated}/GettingStartedDocumentation/createNoteResponse.asciidoc[] +include::{generated}/get-note/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}/GettingStartedDocumentation/getTagsForExistingNoteRequestResponse.asciidoc[] +include::{generated}/get-tags-for-existing-note/request-response.asciidoc[] diff --git a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java b/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java index 4c2424a6..ab876de1 100644 --- a/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java +++ b/rest-notes-spring-hateoas/src/documentation/java/com/example/notes/GettingStartedDocumentation.java @@ -25,6 +25,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.core.RestDocumentation.document; import java.io.UnsupportedEncodingException; import java.util.Arrays; @@ -39,7 +40,7 @@ import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; import org.springframework.restdocs.core.RestDocumentationConfiguration; -import org.springframework.restdocs.core.RestDocumentationJUnit4ClassRunner; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -50,7 +51,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; -@RunWith(RestDocumentationJUnit4ClassRunner.class) +@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class GettingStartedDocumentation { @@ -71,10 +72,13 @@ public class GettingStartedDocumentation { @Test public void index() throws Exception { - this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("links[?(@.rel==notes)]", is(notNullValue()))) - .andExpect(jsonPath("links[?(@.rel==tags)]", is(notNullValue()))); + document( + "index", + this.mockMvc + .perform(get("/").accept(MediaTypes.HAL_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("links[?(@.rel==notes)]", is(notNullValue()))) + .andExpect(jsonPath("links[?(@.rel==tags)]", is(notNullValue())))); } @Test @@ -99,41 +103,51 @@ public class GettingStartedDocumentation { note.put("title", "Note creation with cURL"); note.put("body", "An example of how to create a note using cURL"); - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())).andReturn() - .getResponse().getHeader("Location"); + String noteLocation = document( + "create-note", + this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue()))) + .andReturn().getResponse().getHeader("Location"); return noteLocation; } void getNote(String noteLocation) throws Exception { - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("links[?(@.rel==tags)]", is(notNullValue()))); + document( + "get-note", + this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("links[?(@.rel==tags)]", is(notNullValue())))); } String createTag() throws Exception, JsonProcessingException { Map tag = new HashMap(); tag.put("name", "getting-started"); - String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())).andReturn() - .getResponse().getHeader("Location"); + String tagLocation = document( + "create-tag", + this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue()))) + .andReturn().getResponse().getHeader("Location"); return tagLocation; } void getTag(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("name", is(notNullValue()))) - .andExpect(jsonPath("links[?(@.rel==notes)]", is(notNullValue()))); + document( + "get-tag", + this.mockMvc + .perform(get(tagLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("name", is(notNullValue()))) + .andExpect(jsonPath("links[?(@.rel==notes)]", is(notNullValue())))); } String createTaggedNote(String tag) throws Exception { @@ -142,51 +156,61 @@ public class GettingStartedDocumentation { note.put("body", "An example of how to create a tagged note using cURL"); note.put("tags", Arrays.asList(tag)); - String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaType.APPLICATION_JSON).content( - objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()) - .andExpect(header().string("Location", notNullValue())).andReturn() - .getResponse().getHeader("Location"); + String noteLocation = document( + "create-tagged-note", + this.mockMvc + .perform( + post("/notes").contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andExpect(header().string("Location", notNullValue()))) + .andReturn().getResponse().getHeader("Location"); return noteLocation; } void getTaggedNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(notNullValue()))) - .andExpect(jsonPath("body", is(notNullValue()))) - .andExpect(jsonPath("links[?(@.rel==tags)]", is(notNullValue()))); + document( + "get-tagged-note", + this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("title", is(notNullValue()))) + .andExpect(jsonPath("body", is(notNullValue()))) + .andExpect(jsonPath("links[?(@.rel==tags)]", is(notNullValue())))); } void getTags(String taggedNoteLocation) throws Exception { String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) .andReturn(), "tags"); - this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("links[?(@.rel==tag)]", is(notNullValue()))); + document("get-tags", + this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("links[?(@.rel==tag)]", is(notNullValue())))); } void tagExistingNote(String noteLocation, String tagLocation) throws Exception { Map update = new HashMap(); update.put("tags", Arrays.asList(tagLocation)); - this.mockMvc - .perform( - patch(noteLocation).contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(update))) - .andDo(print()).andExpect(status().isNoContent()); + document( + "tag-existing-note", + this.mockMvc + .perform( + patch(noteLocation).contentType( + MediaType.APPLICATION_JSON).content( + objectMapper.writeValueAsString(update))) + .andDo(print()).andExpect(status().isNoContent())); } void getTaggedExistingNote(String tagLocation) throws Exception { - this.mockMvc.perform(get(tagLocation)).andExpect(status().isOk()); + document("get-tagged-existing-note", this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk())); } void getTagsForExistingNote(String taggedNoteLocation) throws Exception { String tagsLocation = getLink(this.mockMvc.perform(get(taggedNoteLocation)) .andReturn(), "tags"); - this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("links[?(@.rel==tag)]", is(notNullValue()))); + document("get-tags-for-existing-note", + this.mockMvc.perform(get(tagsLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("links[?(@.rel==tag)]", is(notNullValue())))); } private String getLink(MvcResult result, String rel) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationContext.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationContext.java deleted file mode 100644 index 2b3ef0b1..00000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/DocumentationContext.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2014 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.core; - -import java.lang.reflect.Method; -import java.util.Stack; - -class DocumentationContext { - - private static final InheritableThreadLocal> CONTEXTS = new InheritableThreadLocal>() { - - @Override - protected Stack initialValue() { - return new Stack(); - } - - }; - - private final Class documentationClass; - - private final Method documentationMethod; - - public DocumentationContext(Class documentationClass, Method documentationMethod) { - this.documentationClass = documentationClass; - this.documentationMethod = documentationMethod; - } - - public static DocumentationContext current() { - return CONTEXTS.get().peek(); - } - - public Class getDocumentationClass() { - return documentationClass; - } - - public Method getDocumentationMethod() { - return documentationMethod; - } - - static void push(DocumentationContext context) { - CONTEXTS.get().push(context); - } - - static void pop() { - CONTEXTS.get().pop(); - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java new file mode 100644 index 00000000..6439b9bb --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentation.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 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.core; + +import org.springframework.test.web.servlet.ResultActions; + +import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequest; +import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequestAndResponse; +import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlResponse; + +public class RestDocumentation { + + public static ResultActions document(String outputDir, ResultActions resultActions) + throws Exception { + return resultActions + .andDo(documentCurlRequest(outputDir).includeResponseHeaders()) + .andDo(documentCurlResponse(outputDir).includeResponseHeaders()) + .andDo(documentCurlRequestAndResponse(outputDir).includeResponseHeaders()); + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java index 50093fc3..1fdd1d44 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationConfiguration.java @@ -16,17 +16,13 @@ package org.springframework.restdocs.core; -import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequest; -import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlRequestAndResponse; -import static org.springframework.restdocs.core.RestDocumentationResultHandlers.documentCurlResponse; - import org.springframework.mock.web.MockHttpServletRequest; 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.web.context.WebApplicationContext; -public class RestDocumentationConfiguration implements MockMvcConfigurer { +public class RestDocumentationConfiguration extends MockMvcConfigurerAdapter { private String scheme = "http"; @@ -49,13 +45,6 @@ public class RestDocumentationConfiguration implements MockMvcConfigurer { return this; } - @Override - public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { - builder.alwaysDo(documentCurlRequest().includeResponseHeaders()) - .alwaysDo(documentCurlResponse().includeResponseHeaders()) - .alwaysDo(documentCurlRequestAndResponse().includeResponseHeaders()); - } - @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationJUnit4ClassRunner.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationJUnit4ClassRunner.java deleted file mode 100644 index 18577e42..00000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationJUnit4ClassRunner.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2014 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.core; - -import java.lang.reflect.Method; - -import org.junit.runners.model.InitializationError; -import org.springframework.cglib.proxy.Enhancer; -import org.springframework.cglib.proxy.MethodInterceptor; -import org.springframework.cglib.proxy.MethodProxy; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -public class RestDocumentationJUnit4ClassRunner extends SpringJUnit4ClassRunner { - - public RestDocumentationJUnit4ClassRunner(Class clazz) throws InitializationError { - super(clazz); - } - - protected Object createTest() throws Exception { - Object testInstance = createProxiedTestInstance(); - getTestContextManager().prepareTestInstance(testInstance); - return testInstance; - } - - private Object createProxiedTestInstance() { - Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(getTestClass().getJavaClass()); - enhancer.setClassLoader(getTestClass().getJavaClass().getClassLoader()); - enhancer.setCallback(new DocumentationContextManagingMethodInterceptor( - getTestClass().getJavaClass())); - return enhancer.create(); - } - - private static class DocumentationContextManagingMethodInterceptor implements - MethodInterceptor { - - private final Class testClass; - - private DocumentationContextManagingMethodInterceptor(Class testClass) { - this.testClass = testClass; - } - - @Override - public Object intercept(Object target, Method method, Object[] args, - MethodProxy methodProxy) throws Throwable { - DocumentationContext.push(new DocumentationContext(this.testClass, method)); - try { - return methodProxy.invokeSuper(target, args); - } - finally { - DocumentationContext.pop(); - } - } - - } -} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java index e5e390ce..f30b77fc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/core/RestDocumentationResultHandlers.java @@ -35,8 +35,8 @@ import org.springframework.web.bind.annotation.RequestMethod; public abstract class RestDocumentationResultHandlers { - public static CurlResultHandler documentCurlRequest() { - return new CurlResultHandler("Request.asciidoc") { + public static CurlResultHandler documentCurlRequest(String outputDir) { + return new CurlResultHandler(outputDir, "request") { @Override public void handle(MvcResult result, DocumentationWriter writer) throws Exception { @@ -46,8 +46,8 @@ public abstract class RestDocumentationResultHandlers { }; } - public static CurlResultHandler documentCurlResponse() { - return new CurlResultHandler("Response.asciidoc") { + public static CurlResultHandler documentCurlResponse(String outputDir) { + return new CurlResultHandler(outputDir, "response") { @Override public void handle(MvcResult result, DocumentationWriter writer) throws Exception { @@ -57,8 +57,8 @@ public abstract class RestDocumentationResultHandlers { }; } - public static CurlResultHandler documentCurlRequestAndResponse() { - return new CurlResultHandler("RequestResponse.asciidoc") { + public static CurlResultHandler documentCurlRequestAndResponse(String outputDir) { + return new CurlResultHandler(outputDir, "request-response") { @Override public void handle(MvcResult result, DocumentationWriter writer) throws Exception { @@ -167,10 +167,13 @@ public abstract class RestDocumentationResultHandlers { private final CurlConfiguration curlConfiguration = new CurlConfiguration(); - private String suffix; + private String outputDir; - public CurlResultHandler(String suffix) { - this.suffix = suffix; + private String fileName; + + public CurlResultHandler(String outputDir, String fileName) { + this.outputDir = outputDir; + this.fileName = fileName; } CurlConfiguration getCurlConfiguration() { @@ -184,7 +187,7 @@ public abstract class RestDocumentationResultHandlers { @Override public void handle(MvcResult result) throws Exception { - PrintStream printStream = createPrintStream(this.suffix); + PrintStream printStream = createPrintStream(); try { handle(result, new DocumentationWriter(printStream)); } @@ -193,16 +196,10 @@ public abstract class RestDocumentationResultHandlers { } } - private PrintStream createPrintStream(String suffix) + private PrintStream createPrintStream() throws FileNotFoundException { - DocumentationContext context = DocumentationContext.current(); - if (context == null) { - throw new IllegalStateException(); - } - - String path = resolveOutputPath(context); - - File outputFile = new File(path); + + File outputFile = new File(this.outputDir, this.fileName + ".asciidoc"); if (!outputFile.isAbsolute()) { outputFile = makeAbsolute(outputFile); } @@ -216,21 +213,7 @@ public abstract class RestDocumentationResultHandlers { outputFile.getPath()); } - private String resolveOutputPath(DocumentationContext context) { - String shortClassName = getShortClassName(context.getDocumentationClass()); - return shortClassName + "/" + context.getDocumentationMethod().getName() + this.suffix; - } - - private String getShortClassName(Class clazz) { - int index = clazz.getName().lastIndexOf('.'); - if (index >= 0) { - return clazz.getName().substring(index + 1); - } - return clazz.getName(); - } - abstract void handle(MvcResult result, DocumentationWriter writer) throws Exception; - } }