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