Rework the API to improve readability and extensibility

This commit updates the API to improve its extensibility and
readability.

SnippetWritingResultHandler has been replaced with a  more general
purpose Snippet interface. Snippets are now provided to the main
document method using varargs rather than the various with… methods
that were previously used. As a result a custom Snippet implementation
can now be used in exactly the same way as any of the built-in
snippets:

this.mockMvc.perform(get("/"))
        .andExpect(status().isOk())
        .andDo(document("index-example",
                links(
                        linkWithRel("notes").description("…"),
                        linkWithRel("tags").description("…")),
                responseFields(
                        fieldWithPath("_links").description("…")),
                yourCustomSnippet()));

Control of the snippets that are generated by default is now available
via RestDocumentationConfigurer:

this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
        .apply(documentationConfiguration().snippets()
               .withDefaults(curlRequest(), yourCustomSnippet()))
        .build();

See gh-73
This commit is contained in:
Andy Wilkinson
2015-07-30 16:29:25 +01:00
parent aaf8aab77e
commit 0c8a71370b
67 changed files with 2326 additions and 1948 deletions

View File

@@ -48,6 +48,26 @@ include::{examples-dir}/com/example/CustomEncoding.java[tags=custom-encoding]
[[configuration-default-snippets]]
=== Default snippets
Three snippets are produced by default:
- `curl-request`
- `http-request`
- `http-response`
This default configuration is applied by `RestDocumentationConfigurer`. You can use its
API to change the configuration. For example, to only produce the `curl-request` snippet
by default:
[source,java,indent=0]
----
include::{examples-dir}/com/example/CustomDefaultSnippetsConfiguration.java[tags=custom-default-snippets]
----
[[configuration-output-directory]] [[configuration-output-directory]]
=== Snippet output directory === Snippet output directory

View File

@@ -15,7 +15,8 @@ https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API:
---- ----
include::{examples-dir}/com/example/Hypermedia.java[tag=links] include::{examples-dir}/com/example/Hypermedia.java[tag=links]
---- ----
<1> Use `withLinks` is to describe the expected links. <1> Produce a snippet describing the response's links. Uses the static `links` method on
`org.springframework.restdocs.hypermedia.HypermediaDocumentation`.
<2> Expect a link whose rel is `alpha`. Uses the static `linkWithRel` method on <2> Expect a link whose rel is `alpha`. Uses the static `linkWithRel` method on
`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. `org.springframework.restdocs.hypermedia.HypermediaDocumentation`.
<3> Expect a link whose rel is `bravo`. <3> Expect a link whose rel is `bravo`.
@@ -40,14 +41,14 @@ Two link formats are understood by default:
content type of the response is compatible with `application/hal+json`. content type of the response is compatible with `application/hal+json`.
If you are using Atom or HAL-format links but with a different content type you can If you are using Atom or HAL-format links but with a different content type you can
provide one of the built-in `LinkExtractor` implementations to `withLinks`. For example: provide one of the built-in `LinkExtractor` implementations to `links`. For example:
[source,java,indent=0] [source,java,indent=0]
---- ----
include::{examples-dir}/com/example/Hypermedia.java[tag=explicit-extractor] include::{examples-dir}/com/example/Hypermedia.java[tag=explicit-extractor]
---- ----
<1> Indicate that the links are in HAL format. Uses the static `halLinks` method on <1> Indicate that the links are in HAL format. Uses the static `halLinks` method on
`org.springframework.restdocs.hypermedia.LinkExtractors`. `org.springframework.restdocs.hypermedia.HypermediaDocumentation`.
If your API represents its links in a format other than Atom or HAL you can provide your If your API represents its links in a format other than Atom or HAL you can provide your
own implementation of the `LinkExtractor` interface to extract the links from the own implementation of the `LinkExtractor` interface to extract the links from the
@@ -176,13 +177,14 @@ include::{examples-dir}/com/example/Payload.java[tags=explicit-type]
[[documenting-your-api-query-parameters]] [[documenting-your-api-query-parameters]]
=== Query parameters === Query parameters
A request's query parameters can be documented using `withQueryParameters` A request's query parameters can be documented using `queryParameters`
[source,java,indent=0] [source,java,indent=0]
---- ----
include::{examples-dir}/com/example/QueryParameters.java[tags=query-parameters] include::{examples-dir}/com/example/QueryParameters.java[tags=query-parameters]
---- ----
<1> Use `withQueryParameters` to describe the query parameters. <1> Produce a snippet describing the request's query parameters. Uses the static
`queryParameters` method on `org.springframework.restdocs.request.RequestDocumentation`.
<2> Document a parameter named `page`. Uses the static `parameterWithName` method on <2> Document a parameter named `page`. Uses the static `parameterWithName` method on
`org.springframework.restdocs.request.RequestDocumentation`. `org.springframework.restdocs.request.RequestDocumentation`.
<3> Document a parameter named `per_page`. <3> Document a parameter named `per_page`.
@@ -199,14 +201,15 @@ is not found in the request.
[[documenting-your-api-path-parameters]] [[documenting-your-api-path-parameters]]
=== Path parameters === Path parameters
A request's path parameters can be documented using `withPathParameters` A request's path parameters can be documented using `pathParameters`
[source,java,indent=0] [source,java,indent=0]
---- ----
include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters]
---- ----
<1> Build the request. Uses the static `get` method on `RestDocumentationRequestBuilders`. <1> Build the request. Uses the static `get` method on `RestDocumentationRequestBuilders`.
<2> Use `withPathParameters` to describe the path parameters. <2> Produce a snippet describing the request's path parameters. Uses the static
`pathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`.
<3> Document a parameter named `longitude`. Uses the static `parameterWithName` method on <3> Document a parameter named `longitude`. Uses the static `parameterWithName` method on
`org.springframework.restdocs.request.RequestDocumentation`. `org.springframework.restdocs.request.RequestDocumentation`.
<4> Document a parameter named `latitude`. <4> Document a parameter named `latitude`.
@@ -244,6 +247,9 @@ documented
| Contains the HTTP response that was returned | Contains the HTTP response that was returned
|=== |===
You can configure which snippets are produced by default. Please refer to the
<<configuration, configuration section>> for more information.
[[documentating-your-api-parameterized-output-directories]] [[documentating-your-api-parameterized-output-directories]]

View File

@@ -200,7 +200,7 @@ be included in the project's jar:
<goals> <goals>
<goal>copy-resources</goal> <goal>copy-resources</goal>
</goals> </goals>
<configuration> <configuration>
<outputDirectory> <outputDirectory>
${project.build.outputDirectory}/static/docs ${project.build.outputDirectory}/static/docs
</outputDirectory> </outputDirectory>
@@ -245,7 +245,7 @@ The `MockMvc` instance is configured using a `RestDocumentationConfigurer`. An i
of this class can be obtained from the static `documentationConfiguration()` method on of this class can be obtained from the static `documentationConfiguration()` method on
`org.springframework.restdocs.RestDocumentation`. `RestDocumentationConfigurer` applies `org.springframework.restdocs.RestDocumentation`. `RestDocumentationConfigurer` applies
sensible defaults and also provides an API for customizing the configuration. Refer to the sensible defaults and also provides an API for customizing the configuration. Refer to the
<<configuration, Configuration section>> for more information. <<configuration, configuration section>> for more information.

View File

@@ -0,0 +1,29 @@
package com.example;
import static org.springframework.restdocs.RestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest;
import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
public class CustomDefaultSnippetsConfiguration {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@Before
public void setUp() {
// tag::custom-default-snippets[]
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration().snippets()
.withDefaults(curlRequest()))
.build();
// end::custom-default-snippets[]
}
}

View File

@@ -20,7 +20,8 @@ import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.LinkExtractors.halLinks; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -29,13 +30,13 @@ public class Hypermedia {
private MockMvc mockMvc; private MockMvc mockMvc;
public void links() throws Exception { public void defaultExtractor() throws Exception {
// tag::links[] // tag::links[]
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("index").withLinks( // <1> .andDo(document("index", links( // <1>
linkWithRel("alpha").description("Link to the alpha resource"), // <2> linkWithRel("alpha").description("Link to the alpha resource"), // <2>
linkWithRel("bravo").description("Link to the bravo resource"))); // <3> linkWithRel("bravo").description("Link to the bravo resource")))); // <3>
// end::links[] // end::links[]
} }
@@ -43,9 +44,9 @@ public class Hypermedia {
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
//tag::explicit-extractor[] //tag::explicit-extractor[]
.andDo(document("index").withLinks(halLinks(), // <1> .andDo(document("index", links(halLinks(), // <1>
linkWithRel("alpha").description("Link to the alpha resource"), linkWithRel("alpha").description("Link to the alpha resource"),
linkWithRel("bravo").description("Link to the bravo resource"))); linkWithRel("bravo").description("Link to the bravo resource"))));
// end::explicit-extractor[] // end::explicit-extractor[]
} }

View File

@@ -18,6 +18,7 @@ package com.example;
import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -27,14 +28,14 @@ public class PathParameters {
private MockMvc mockMvc; private MockMvc mockMvc;
public void pathParameters() throws Exception { public void pathParametersSnippet() throws Exception {
// tag::path-parameters[] // tag::path-parameters[]
this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) // <1> this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) // <1>
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("locations").withPathParameters( // <2> .andDo(document("locations", pathParameters( // <2>
parameterWithName("latitude").description("The location's latitude"), // <3> parameterWithName("latitude").description("The location's latitude"), // <3>
parameterWithName("longitude").description("The location's longitude") // <4> parameterWithName("longitude").description("The location's longitude") // <4>
)); )));
// end::path-parameters[] // end::path-parameters[]
} }

View File

@@ -18,6 +18,8 @@ package com.example;
import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
@@ -26,7 +28,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.FieldType; import org.springframework.restdocs.payload.FieldType;
import org.springframework.restdocs.snippet.Attributes;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
public class Payload { public class Payload {
@@ -37,9 +38,9 @@ private MockMvc mockMvc;
// tag::response[] // tag::response[]
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("index").withResponseFields( // <1> .andDo(document("index", responseFields( // <1>
fieldWithPath("contact").description("The user's contact details"), // <2> fieldWithPath("contact").description("The user's contact details"), // <2>
fieldWithPath("contact.email").description("The user's email address"))); // <3> fieldWithPath("contact.email").description("The user's email address")))); // <3>
// end::response[] // end::response[]
} }
@@ -47,11 +48,11 @@ private MockMvc mockMvc;
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
// tag::explicit-type[] // tag::explicit-type[]
.andDo(document("index").withResponseFields( .andDo(document("index", responseFields(
fieldWithPath("contact.email") fieldWithPath("contact.email")
.type(FieldType.STRING) // <1> .type(FieldType.STRING) // <1>
.optional() .optional()
.description("The user's email address"))); .description("The user's email address"))));
// end::explicit-type[] // end::explicit-type[]
} }
@@ -59,7 +60,7 @@ private MockMvc mockMvc;
this.mockMvc.perform(post("/users/").accept(MediaType.APPLICATION_JSON)) this.mockMvc.perform(post("/users/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
// tag::constraints[] // tag::constraints[]
.andDo(document("create-user").withRequestFields( .andDo(document("create-user", requestFields(
attributes( attributes(
key("title").value("Fields for user creation")), // <1> key("title").value("Fields for user creation")), // <1>
fieldWithPath("name") fieldWithPath("name")
@@ -69,7 +70,7 @@ private MockMvc mockMvc;
fieldWithPath("email") fieldWithPath("email")
.description("The user's email address") .description("The user's email address")
.attributes( .attributes(
key("constraints").value("Must be a valid email address")))); // <3> key("constraints").value("Must be a valid email address"))))); // <3>
// end::constraints[] // end::constraints[]
} }

View File

@@ -18,6 +18,7 @@ package com.example;
import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -27,14 +28,14 @@ public class QueryParameters {
private MockMvc mockMvc; private MockMvc mockMvc;
public void queryParameters() throws Exception { public void queryParametersSnippet() throws Exception {
// tag::query-parameters[] // tag::query-parameters[]
this.mockMvc.perform(get("/users?page=2&per_page=100")) this.mockMvc.perform(get("/users?page=2&per_page=100"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("users").withQueryParameters( // <1> .andDo(document("users", queryParameters( // <1>
parameterWithName("page").description("The page to retrieve"), // <2> parameterWithName("page").description("The page to retrieve"), // <2>
parameterWithName("per_page").description("Entries per page") // <3> parameterWithName("per_page").description("Entries per page") // <3>
)); )));
// end::query-parameters[] // end::query-parameters[]
} }

View File

@@ -21,7 +21,10 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; import static org.springframework.restdocs.RestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
@@ -100,26 +103,26 @@ public class ApiDocumentation {
.andExpect(jsonPath("timestamp", is(notNullValue()))) .andExpect(jsonPath("timestamp", is(notNullValue())))
.andExpect(jsonPath("status", is(400))) .andExpect(jsonPath("status", is(400)))
.andExpect(jsonPath("path", is(notNullValue()))) .andExpect(jsonPath("path", is(notNullValue())))
.andDo(document("error-example") .andDo(document("error-example",
.withResponseFields( responseFields(
fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"), fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"),
fieldWithPath("message").description("A description of the cause of the error"), fieldWithPath("message").description("A description of the cause of the error"),
fieldWithPath("path").description("The path to which the request was made"), fieldWithPath("path").description("The path to which the request was made"),
fieldWithPath("status").description("The HTTP status code, e.g. `400`"), fieldWithPath("status").description("The HTTP status code, e.g. `400`"),
fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred"))); fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred"))));
} }
@Test @Test
public void indexExample() throws Exception { public void indexExample() throws Exception {
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("index-example") .andDo(document("index-example",
.withLinks( links(
linkWithRel("notes").description("The <<resources-notes,Notes resource>>"), linkWithRel("notes").description("The <<resources-notes,Notes resource>>"),
linkWithRel("tags").description("The <<resources-tags,Tags resource>>"), linkWithRel("tags").description("The <<resources-tags,Tags resource>>"),
linkWithRel("profile").description("The ALPS profile for the service")) linkWithRel("profile").description("The ALPS profile for the service")),
.withResponseFields( responseFields(
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources"))); fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources"))));
} }
@@ -135,9 +138,9 @@ public class ApiDocumentation {
this.mockMvc.perform(get("/notes")) this.mockMvc.perform(get("/notes"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("notes-list-example") .andDo(document("notes-list-example",
.withResponseFields( responseFields(
fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"))); fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"))));
} }
@Test @Test
@@ -161,11 +164,11 @@ public class ApiDocumentation {
post("/notes").contentType(MediaTypes.HAL_JSON).content( post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note))).andExpect( this.objectMapper.writeValueAsString(note))).andExpect(
status().isCreated()) status().isCreated())
.andDo(document("notes-create-example") .andDo(document("notes-create-example",
.withRequestFields( requestFields(
fieldWithPath("title").description("The title of the note"), fieldWithPath("title").description("The title of the note"),
fieldWithPath("body").description("The body of the note"), fieldWithPath("body").description("The body of the note"),
fieldWithPath("tags").description("An array of tag resource URIs"))); fieldWithPath("tags").description("An array of tag resource URIs"))));
} }
@Test @Test
@@ -198,14 +201,14 @@ public class ApiDocumentation {
.andExpect(jsonPath("body", is(note.get("body")))) .andExpect(jsonPath("body", is(note.get("body"))))
.andExpect(jsonPath("_links.self.href", is(noteLocation))) .andExpect(jsonPath("_links.self.href", is(noteLocation)))
.andExpect(jsonPath("_links.tags", is(notNullValue()))) .andExpect(jsonPath("_links.tags", is(notNullValue())))
.andDo(document("note-get-example") .andDo(document("note-get-example",
.withLinks( links(
linkWithRel("self").description("This <<resources-note,note>>"), linkWithRel("self").description("This <<resources-note,note>>"),
linkWithRel("tags").description("This note's tags")) linkWithRel("tags").description("This note's tags")),
.withResponseFields( responseFields(
fieldWithPath("title").description("The title of the note"), fieldWithPath("title").description("The title of the note"),
fieldWithPath("body").description("The body of the note"), fieldWithPath("body").description("The body of the note"),
fieldWithPath("_links").description("<<resources-note-links,Links>> to other resources"))); fieldWithPath("_links").description("<<resources-note-links,Links>> to other resources"))));
} }
@Test @Test
@@ -219,9 +222,9 @@ public class ApiDocumentation {
this.mockMvc.perform(get("/tags")) this.mockMvc.perform(get("/tags"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("tags-list-example") .andDo(document("tags-list-example",
.withResponseFields( responseFields(
fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"))); fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"))));
} }
@Test @Test
@@ -233,9 +236,9 @@ public class ApiDocumentation {
post("/tags").contentType(MediaTypes.HAL_JSON).content( post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag))) this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
.andDo(document("tags-create-example") .andDo(document("tags-create-example",
.withRequestFields( requestFields(
fieldWithPath("name").description("The name of the tag"))); fieldWithPath("name").description("The name of the tag"))));
} }
@Test @Test
@@ -274,12 +277,11 @@ public class ApiDocumentation {
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(noteUpdate))) this.objectMapper.writeValueAsString(noteUpdate)))
.andExpect(status().isNoContent()) .andExpect(status().isNoContent())
.andDo(document("note-update-example") .andDo(document("note-update-example",
.withRequestFields( requestFields(
fieldWithPath("title").description("The title of the note").type(FieldType.STRING).optional(), fieldWithPath("title").description("The title of the note").type(FieldType.STRING).optional(),
fieldWithPath("body").description("The body of the note").type(FieldType.STRING).optional(), fieldWithPath("body").description("The body of the note").type(FieldType.STRING).optional(),
fieldWithPath("tags").description("An array of tag resource URIs").optional())); fieldWithPath("tags").description("An array of tag resource URIs").optional())));
} }
@Test @Test
@@ -297,13 +299,13 @@ public class ApiDocumentation {
this.mockMvc.perform(get(tagLocation)) this.mockMvc.perform(get(tagLocation))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("name", is(tag.get("name")))) .andExpect(jsonPath("name", is(tag.get("name"))))
.andDo(document("tag-get-example") .andDo(document("tag-get-example",
.withLinks( links(
linkWithRel("self").description("This <<resources-tag,tag>>"), linkWithRel("self").description("This <<resources-tag,tag>>"),
linkWithRel("notes").description("The <<resources-tagged-notes,notes>> that have this tag")) linkWithRel("notes").description("The <<resources-tagged-notes,notes>> that have this tag")),
.withResponseFields( responseFields(
fieldWithPath("name").description("The name of the tag"), fieldWithPath("name").description("The name of the tag"),
fieldWithPath("_links").description("<<resources-tag-links,Links>> to other resources"))); fieldWithPath("_links").description("<<resources-tag-links,Links>> to other resources"))));
} }
@Test @Test
@@ -325,9 +327,9 @@ public class ApiDocumentation {
patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tagUpdate))) this.objectMapper.writeValueAsString(tagUpdate)))
.andExpect(status().isNoContent()) .andExpect(status().isNoContent())
.andDo(document("tag-update-example") .andDo(document("tag-update-example",
.withRequestFields( requestFields(
fieldWithPath("name").description("The name of the tag"))); fieldWithPath("name").description("The name of the tag"))));
} }
private void createNote(String title, String body) { private void createNote(String title, String body) {

View File

@@ -21,7 +21,10 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.RestDocumentation.documentationConfiguration; import static org.springframework.restdocs.RestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
@@ -89,25 +92,25 @@ public class ApiDocumentation {
.andExpect(jsonPath("timestamp", is(notNullValue()))) .andExpect(jsonPath("timestamp", is(notNullValue())))
.andExpect(jsonPath("status", is(400))) .andExpect(jsonPath("status", is(400)))
.andExpect(jsonPath("path", is(notNullValue()))) .andExpect(jsonPath("path", is(notNullValue())))
.andDo(document("error-example") .andDo(document("error-example",
.withResponseFields( responseFields(
fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"), fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"),
fieldWithPath("message").description("A description of the cause of the error"), fieldWithPath("message").description("A description of the cause of the error"),
fieldWithPath("path").description("The path to which the request was made"), fieldWithPath("path").description("The path to which the request was made"),
fieldWithPath("status").description("The HTTP status code, e.g. `400`"), fieldWithPath("status").description("The HTTP status code, e.g. `400`"),
fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred"))); fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred"))));
} }
@Test @Test
public void indexExample() throws Exception { public void indexExample() throws Exception {
this.mockMvc.perform(get("/")) this.mockMvc.perform(get("/"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("index-example") .andDo(document("index-example",
.withLinks( links(
linkWithRel("notes").description("The <<resources-notes,Notes resource>>"), linkWithRel("notes").description("The <<resources-notes,Notes resource>>"),
linkWithRel("tags").description("The <<resources-tags,Tags resource>>")) linkWithRel("tags").description("The <<resources-tags,Tags resource>>")),
.withResponseFields( responseFields(
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources"))); fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources"))));
} }
@Test @Test
@@ -122,9 +125,9 @@ public class ApiDocumentation {
this.mockMvc.perform(get("/notes")) this.mockMvc.perform(get("/notes"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("notes-list-example") .andDo(document("notes-list-example",
.withResponseFields( responseFields(
fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"))); fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"))));
} }
@Test @Test
@@ -148,11 +151,11 @@ public class ApiDocumentation {
post("/notes").contentType(MediaTypes.HAL_JSON).content( post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note))) this.objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
.andDo(document("notes-create-example") .andDo(document("notes-create-example",
.withRequestFields( requestFields(
fieldWithPath("title").description("The title of the note"), fieldWithPath("title").description("The title of the note"),
fieldWithPath("body").description("The body of the note"), fieldWithPath("body").description("The body of the note"),
fieldWithPath("tags").description("An array of tag resource URIs"))); fieldWithPath("tags").description("An array of tag resource URIs"))));
} }
@Test @Test
@@ -185,14 +188,14 @@ public class ApiDocumentation {
.andExpect(jsonPath("body", is(note.get("body")))) .andExpect(jsonPath("body", is(note.get("body"))))
.andExpect(jsonPath("_links.self.href", is(noteLocation))) .andExpect(jsonPath("_links.self.href", is(noteLocation)))
.andExpect(jsonPath("_links.note-tags", is(notNullValue()))) .andExpect(jsonPath("_links.note-tags", is(notNullValue())))
.andDo(document("note-get-example") .andDo(document("note-get-example",
.withLinks( links(
linkWithRel("self").description("This <<resources-note,note>>"), linkWithRel("self").description("This <<resources-note,note>>"),
linkWithRel("note-tags").description("This note's <<resources-note-tags,tags>>")) linkWithRel("note-tags").description("This note's <<resources-note-tags,tags>>")),
.withResponseFields( responseFields(
fieldWithPath("title").description("The title of the note"), fieldWithPath("title").description("The title of the note"),
fieldWithPath("body").description("The body of the note"), fieldWithPath("body").description("The body of the note"),
fieldWithPath("_links").description("<<resources-note-links,Links>> to other resources"))); fieldWithPath("_links").description("<<resources-note-links,Links>> to other resources"))));
} }
@@ -207,9 +210,9 @@ public class ApiDocumentation {
this.mockMvc.perform(get("/tags")) this.mockMvc.perform(get("/tags"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("tags-list-example") .andDo(document("tags-list-example",
.withResponseFields( responseFields(
fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"))); fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"))));
} }
@Test @Test
@@ -221,9 +224,9 @@ public class ApiDocumentation {
post("/tags").contentType(MediaTypes.HAL_JSON).content( post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag))) this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated()) .andExpect(status().isCreated())
.andDo(document("tags-create-example") .andDo(document("tags-create-example",
.withRequestFields( requestFields(
fieldWithPath("name").description("The name of the tag"))); fieldWithPath("name").description("The name of the tag"))));
} }
@Test @Test
@@ -262,11 +265,11 @@ public class ApiDocumentation {
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(noteUpdate))) this.objectMapper.writeValueAsString(noteUpdate)))
.andExpect(status().isNoContent()) .andExpect(status().isNoContent())
.andDo(document("note-update-example") .andDo(document("note-update-example",
.withRequestFields( requestFields(
fieldWithPath("title").description("The title of the note").type(FieldType.STRING).optional(), fieldWithPath("title").description("The title of the note").type(FieldType.STRING).optional(),
fieldWithPath("body").description("The body of the note").type(FieldType.STRING).optional(), fieldWithPath("body").description("The body of the note").type(FieldType.STRING).optional(),
fieldWithPath("tags").description("An array of tag resource URIs").optional())); fieldWithPath("tags").description("An array of tag resource URIs").optional())));
} }
@Test @Test
@@ -284,13 +287,13 @@ public class ApiDocumentation {
this.mockMvc.perform(get(tagLocation)) this.mockMvc.perform(get(tagLocation))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("name", is(tag.get("name")))) .andExpect(jsonPath("name", is(tag.get("name"))))
.andDo(document("tag-get-example") .andDo(document("tag-get-example",
.withLinks( links(
linkWithRel("self").description("This <<resources-tag,tag>>"), linkWithRel("self").description("This <<resources-tag,tag>>"),
linkWithRel("tagged-notes").description("The <<resources-tagged-notes,notes>> that have this tag")) linkWithRel("tagged-notes").description("The <<resources-tagged-notes,notes>> that have this tag")),
.withResponseFields( responseFields(
fieldWithPath("name").description("The name of the tag"), fieldWithPath("name").description("The name of the tag"),
fieldWithPath("_links").description("<<resources-tag-links,Links>> to other resources"))); fieldWithPath("_links").description("<<resources-tag-links,Links>> to other resources"))));
} }
@Test @Test
@@ -312,9 +315,9 @@ public class ApiDocumentation {
patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tagUpdate))) this.objectMapper.writeValueAsString(tagUpdate)))
.andExpect(status().isNoContent()) .andExpect(status().isNoContent())
.andDo(document("tag-update-example") .andDo(document("tag-update-example",
.withRequestFields( requestFields(
fieldWithPath("name").description("The name of the tag"))); fieldWithPath("name").description("The name of the tag"))));
} }
private void createNote(String title, String body) { private void createNote(String title, String body) {

View File

@@ -27,6 +27,7 @@ import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.BridgeMethodResolver; import org.springframework.core.BridgeMethodResolver;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.response.ResponsePostProcessor; import org.springframework.restdocs.response.ResponsePostProcessor;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@@ -47,19 +48,22 @@ public final class ResponseModifier {
/** /**
* Provides a {@link RestDocumentationResultHandler} that can be used to document the * Provides a {@link RestDocumentationResultHandler} that can be used to document the
* request and modified result. * request and modified response.
* @param identifier An identifier for the API call that is being documented * @param identifier an identifier for the API call that is being documented
* @param snippets the snippets to use to document the call
* @return the result handler that will produce the documentation * @return the result handler that will produce the documentation
*/ */
public RestDocumentationResultHandler andDocument(String identifier) { public RestDocumentationResultHandler andDocument(String identifier,
return new ResponseModifyingRestDocumentationResultHandler(identifier); Snippet... snippets) {
return new ResponseModifyingRestDocumentationResultHandler(identifier, snippets);
} }
class ResponseModifyingRestDocumentationResultHandler extends class ResponseModifyingRestDocumentationResultHandler extends
RestDocumentationResultHandler { RestDocumentationResultHandler {
public ResponseModifyingRestDocumentationResultHandler(String identifier) { private ResponseModifyingRestDocumentationResultHandler(String identifier,
super(identifier); Snippet... snippets) {
super(identifier, snippets);
} }
@Override @Override

View File

@@ -19,6 +19,7 @@ package org.springframework.restdocs;
import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.restdocs.config.RestDocumentationConfigurer;
import org.springframework.restdocs.response.ResponsePostProcessor; import org.springframework.restdocs.response.ResponsePostProcessor;
import org.springframework.restdocs.response.ResponsePostProcessors; import org.springframework.restdocs.response.ResponsePostProcessors;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultActions;
@@ -39,6 +40,7 @@ public abstract class RestDocumentation {
/** /**
* Provides access to a {@link MockMvcConfigurer} that can be used to configure the * Provides access to a {@link MockMvcConfigurer} that can be used to configure the
* REST documentation when building a {@link MockMvc} instance. * REST documentation when building a {@link MockMvc} instance.
*
* @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer)
* @return the configurer * @return the configurer
*/ */
@@ -47,15 +49,18 @@ public abstract class RestDocumentation {
} }
/** /**
* Documents the API call using the given {@code identifier}. * Documents the API call with the given {@code identifier} using the given
* {@code handlers}.
* *
* @param identifier An identifier for the API call that is being documented * @param identifier an identifier for the API call that is being documented
* @param snippets the snippets that will document the API call
* @return a Mock MVC {@code ResultHandler} that will produce the documentation * @return a Mock MVC {@code ResultHandler} that will produce the documentation
* @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder) * @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder)
* @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler) * @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler)
*/ */
public static RestDocumentationResultHandler document(String identifier) { public static RestDocumentationResultHandler document(String identifier,
return new RestDocumentationResultHandler(identifier); Snippet... snippets) {
return new RestDocumentationResultHandler(identifier, snippets);
} }
/** /**

View File

@@ -19,6 +19,7 @@ package org.springframework.restdocs;
import java.net.URI; import java.net.URI;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.restdocs.request.RequestDocumentation;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@@ -26,13 +27,13 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
/** /**
* A drop-in replacement for {@link MockMvcRequestBuilders} that captures a request's URL * A drop-in replacement for {@link MockMvcRequestBuilders} that captures a request's URL
* template and makes it available for documentation. Required when * template and makes it available for documentation. Required when
* {@link RestDocumentationResultHandler#withPathParameters(org.springframework.restdocs .request.ParameterDescriptor...) * {@link RequestDocumentation#pathParameters(org.springframework.restdocs.request.ParameterDescriptor...)
* documenting path parameters} and recommended for general usage. * ) documenting path parameters} and recommended for general usage.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @see MockMvcRequestBuilders * @see MockMvcRequestBuilders
* @see RestDocumentationResultHandler#withPathParameters(org.springframework.restdocs.request.ParameterDescriptor...) * @see RequestDocumentation#pathParameters(org.springframework.restdocs.request.ParameterDescriptor...)
* @see RestDocumentationResultHandler#withPathParameters(java.util.Map, * @see RequestDocumentation#pathParameters(java.util.Map,
* org.springframework.restdocs.request.ParameterDescriptor...) * org.springframework.restdocs.request.ParameterDescriptor...)
*/ */
public abstract class RestDocumentationRequestBuilders { public abstract class RestDocumentationRequestBuilders {

View File

@@ -16,28 +16,11 @@
package org.springframework.restdocs; package org.springframework.restdocs;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields;
import static org.springframework.restdocs.request.RequestDocumentation.documentPathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import org.springframework.restdocs.hypermedia.HypermediaDocumentation; import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.hypermedia.LinkDescriptor;
import org.springframework.restdocs.hypermedia.LinkExtractor;
import org.springframework.restdocs.hypermedia.LinkExtractors;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.PayloadDocumentation;
import org.springframework.restdocs.request.ParameterDescriptor;
import org.springframework.restdocs.request.RequestDocumentation;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler; import org.springframework.test.web.servlet.ResultHandler;
@@ -46,317 +29,32 @@ import org.springframework.test.web.servlet.ResultHandler;
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Andreas Evers * @author Andreas Evers
* @see RestDocumentation#document(String) * @see RestDocumentation#document(String, Snippet...)
*/ */
public class RestDocumentationResultHandler implements ResultHandler { public class RestDocumentationResultHandler implements ResultHandler {
private final String identifier; private final String identifier;
private SnippetWritingResultHandler curlRequest; private final List<Snippet> snippets;
private SnippetWritingResultHandler httpRequest; RestDocumentationResultHandler(String identifier, Snippet... snippets) {
private SnippetWritingResultHandler httpResponse;
private List<SnippetWritingResultHandler> delegates = new ArrayList<>();
RestDocumentationResultHandler(String identifier) {
this.identifier = identifier; this.identifier = identifier;
this.curlRequest = documentCurlRequest(this.identifier, null); this.snippets = Arrays.asList(snippets);
this.httpRequest = documentHttpRequest(this.identifier, null);
this.httpResponse = documentHttpResponse(this.identifier, null);
}
/**
* Customizes the default curl request snippet generation to make the given attributes
* available.
*
* @param attributes the attributes
* @return {@code this}
*/
public RestDocumentationResultHandler withCurlRequest(Map<String, Object> attributes) {
this.curlRequest = documentCurlRequest(this.identifier, attributes);
return this;
}
/**
* Customizes the default HTTP request snippet generation to make the given attributes
* available.
*
* @param attributes the attributes
* @return {@code this}
*/
public RestDocumentationResultHandler withHttpRequest(Map<String, Object> attributes) {
this.httpRequest = documentHttpRequest(this.identifier, attributes);
return this;
}
/**
* Customizes the default HTTP response snippet generation to make the given
* attributes available.
*
* @param attributes the attributes
* @return {@code this}
*/
public RestDocumentationResultHandler withHttpResponse(Map<String, Object> attributes) {
this.httpResponse = documentHttpResponse(this.identifier, attributes);
return this;
}
/**
* Document the links in the response using the given {@code descriptors}. The links
* are extracted from the response based on its content type.
* <p>
* If a link is present in the response but is not described by one of the descriptors
* a failure will occur when this handler is invoked. Similarly, if a link is
* described but is not present in the response a failure will also occur when this
* handler is invoked.
*
* @param descriptors the link descriptors
* @return {@code this}
* @see HypermediaDocumentation#linkWithRel(String)
* @see LinkExtractors#extractorForContentType(String)
*/
public RestDocumentationResultHandler withLinks(LinkDescriptor... descriptors) {
return withLinks(null, null, descriptors);
}
/**
* Document the links in the response using the given {@code descriptors}. The links
* are extracted from the response using the given {@code linkExtractor}.
* <p>
* If a link is present in the response but is not described by one of the descriptors
* a failure will occur when this handler is invoked. Similarly, if a link is
* described but is not present in the response a failure will also occur when this
* handler is invoked.
*
* @param linkExtractor used to extract the links from the response
* @param descriptors the link descriptors
* @return {@code this}
* @see HypermediaDocumentation#linkWithRel(String)
*/
public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor,
LinkDescriptor... descriptors) {
return this.withLinks(null, linkExtractor, descriptors);
}
/**
* Document the links in the response using the given {@code descriptors}. The links
* are extracted from the response based on its content type. The given
* {@code attributes} are made available during the generation of the links snippet.
* <p>
* If a link is present in the response but is not described by one of the descriptors
* a failure will occur when this handler is invoked. Similarly, if a link is
* described but is not present in the response a failure will also occur when this
* handler is invoked.
*
* @param attributes the attributes
* @param descriptors the link descriptors
* @return {@code this}
* @see HypermediaDocumentation#linkWithRel(String)
* @see LinkExtractors#extractorForContentType(String)
*/
public RestDocumentationResultHandler withLinks(Map<String, Object> attributes,
LinkDescriptor... descriptors) {
return withLinks(attributes, null, descriptors);
}
/**
* Document the links in the response using the given {@code descriptors}. The links
* are extracted from the response using the given {@code linkExtractor}. The given
* {@code attributes} are made available during the generation of the links snippet.
* <p>
* If a link is present in the response but is not described by one of the descriptors
* a failure will occur when this handler is invoked. Similarly, if a link is
* described but is not present in the response a failure will also occur when this
* handler is invoked.
*
* @param attributes the attributes
* @param linkExtractor used to extract the links from the response
* @param descriptors the link descriptors
* @return {@code this}
* @see HypermediaDocumentation#linkWithRel(String)
*/
public RestDocumentationResultHandler withLinks(Map<String, Object> attributes,
LinkExtractor linkExtractor, LinkDescriptor... descriptors) {
this.delegates.add(documentLinks(this.identifier, attributes, linkExtractor,
descriptors));
return this;
}
/**
* Document the fields in the request using the given {@code descriptors}.
* <p>
* If a field is present in the request but is not documented by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the request a
* failure will also occur. For payloads with a hierarchical structure, documenting a
* field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param descriptors the link descriptors
* @return {@code this}
* @see PayloadDocumentation#fieldWithPath(String)
*/
public RestDocumentationResultHandler withRequestFields(
FieldDescriptor... descriptors) {
return this.withRequestFields(null, descriptors);
}
/**
* Document the fields in the request using the given {@code descriptors}. The given
* {@code attributes} are made available during the generation of the request fields
* snippet.
* <p>
* If a field is present in the request but is not documented by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the request a
* failure will also occur. For payloads with a hierarchical structure, documenting a
* field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param descriptors the link descriptors
* @param attributes the attributes
* @return {@code this}
* @see PayloadDocumentation#fieldWithPath(String)
*/
public RestDocumentationResultHandler withRequestFields(
Map<String, Object> attributes, FieldDescriptor... descriptors) {
this.delegates
.add(documentRequestFields(this.identifier, attributes, descriptors));
return this;
}
/**
* Document the fields in the response using the given {@code descriptors}.
* <p>
* If a field is present in the response but is not documented by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the response
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param descriptors the link descriptors
* @return {@code this}
* @see PayloadDocumentation#fieldWithPath(String)
*/
public RestDocumentationResultHandler withResponseFields(
FieldDescriptor... descriptors) {
return this.withResponseFields(null, descriptors);
}
/**
* Document the fields in the response using the given {@code descriptors}. The given
* {@code attributes} are made available during the generation of the request fields
* snippet.
* <p>
* If a field is present in the response but is not documented by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the response
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param descriptors the link descriptors
* @param attributes the attributes
* @return {@code this}
* @see PayloadDocumentation#fieldWithPath(String)
*/
public RestDocumentationResultHandler withResponseFields(
Map<String, Object> attributes, FieldDescriptor... descriptors) {
this.delegates.add(documentResponseFields(this.identifier, attributes,
descriptors));
return this;
}
/**
* Documents the parameters in the request's query string using the given
* {@code descriptors}.
* <p>
* If a parameter is present in the query string but is not described by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* parameter is described but is not present in the request a failure will also occur
* when this handler is invoked.
*
* @param descriptors the parameter descriptors
* @return {@code this}
* @see RequestDocumentation#parameterWithName(String)
*/
public RestDocumentationResultHandler withQueryParameters(
ParameterDescriptor... descriptors) {
return this.withQueryParameters(null, descriptors);
}
/**
* Documents the parameters in the request's query string using the given
* {@code descriptors}. The given {@code attributes} are made available during the
* generation of the query parameters snippet.
* <p>
* If a parameter is present in the query string but is not described by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* parameter is described but is not present in the request a failure will also occur
* when this handler is invoked.
*
* @param descriptors the parameter descriptors
* @param attributes the attributes
* @return {@code this}
* @see RequestDocumentation#parameterWithName(String)
*/
public RestDocumentationResultHandler withQueryParameters(
Map<String, Object> attributes, ParameterDescriptor... descriptors) {
this.delegates.add(documentQueryParameters(this.identifier, attributes,
descriptors));
return this;
}
/**
* Documents the parameters in the request's path using the given {@code descriptors}.
* <p>
* If a parameter is present in the path but is not described by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* parameter is described but is not present in the request a failure will also occur
* when this handler is invoked.
*
* @param descriptors the parameter descriptors
* @return {@code this}
* @see RequestDocumentation#parameterWithName(String)
*/
public RestDocumentationResultHandler withPathParameters(
ParameterDescriptor... descriptors) {
return this.withPathParameters(null, descriptors);
}
/**
* Documents the parameters in the request's path using the given {@code descriptors}.
* The given {@code attributes} are made available during the generation of the path
* parameters snippet.
* <p>
* If a parameter is present in the path but is not described by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* parameter is described but is not present in the request a failure will also occur
* when this handler is invoked.
*
* @param descriptors the parameter descriptors
* @param attributes the attributes
* @return {@code this}
* @see RequestDocumentation#parameterWithName(String)
*/
public RestDocumentationResultHandler withPathParameters(
Map<String, Object> attributes, ParameterDescriptor... descriptors) {
this.delegates.add(documentPathParameters(this.identifier, attributes,
descriptors));
return this;
} }
@Override @Override
public void handle(MvcResult result) throws Exception { public void handle(MvcResult result) throws Exception {
this.curlRequest.handle(result); for (Snippet snippet : getSnippets(result)) {
this.httpRequest.handle(result); snippet.document(this.identifier, result);
this.httpResponse.handle(result);
for (ResultHandler delegate : this.delegates) {
delegate.handle(result);
} }
} }
@SuppressWarnings("unchecked")
private List<Snippet> getSnippets(MvcResult result) {
List<Snippet> combinedSnippets = new ArrayList<>((List<Snippet>) result.getRequest()
.getAttribute("org.springframework.restdocs.defaultSnippets"));
combinedSnippets.addAll(this.snippets);
return combinedSnippets;
}
} }

View File

@@ -27,7 +27,7 @@ import org.springframework.mock.web.MockHttpServletRequest;
abstract class AbstractConfigurer { abstract class AbstractConfigurer {
/** /**
* Applies the configuration, possibly be modifying the given {@code request} * Applies the configuration, possibly by modifying the given {@code request}
* @param request the request that may be modified * @param request the request that may be modified
*/ */
abstract void apply(MockHttpServletRequest request); abstract void apply(MockHttpServletRequest request);

View File

@@ -49,9 +49,9 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter {
private final RequestPostProcessor requestPostProcessor; private final RequestPostProcessor requestPostProcessor;
private TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); private final TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer();
private WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer();
/** /**
* Creates a new {@link RestDocumentationConfigurer}. * Creates a new {@link RestDocumentationConfigurer}.
@@ -65,19 +65,44 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter {
this.templateEngineConfigurer)); this.templateEngineConfigurer));
} }
/**
* Returns a {@link UriConfigurer} that can be used to configure the request URIs that
* will be documented.
*
* @return the URI configurer
*/
public UriConfigurer uris() { public UriConfigurer uris() {
return this.uriConfigurer; return this.uriConfigurer;
} }
/**
* Returns a {@link SnippetConfigurer} that can be used to configure the snippets that
* will be generated.
*
* @return the snippet configurer
*/
public SnippetConfigurer snippets() { public SnippetConfigurer snippets() {
return this.snippetConfigurer; return this.snippetConfigurer;
} }
/**
* Configures the {@link TemplateEngine} that will be used for snippet rendering.
*
* @param templateEngine the template engine to use
* @return {@code this}
*/
public RestDocumentationConfigurer templateEngine(TemplateEngine templateEngine) { public RestDocumentationConfigurer templateEngine(TemplateEngine templateEngine) {
this.templateEngineConfigurer.setTemplateEngine(templateEngine); this.templateEngineConfigurer.setTemplateEngine(templateEngine);
return this; return this;
} }
/**
* Configures the {@link WriterResolver} that will be used to resolve a writer for a
* snippet.
*
* @param writerResolver The writer resolver to use
* @return {@code this}
*/
public RestDocumentationConfigurer writerResolver(WriterResolver writerResolver) { public RestDocumentationConfigurer writerResolver(WriterResolver writerResolver) {
this.writerResolverConfigurer.setWriterResolver(writerResolver); this.writerResolverConfigurer.setWriterResolver(writerResolver);
return this; return this;
@@ -175,4 +200,5 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter {
} }
} }
} }

View File

@@ -33,10 +33,12 @@ public final class RestDocumentationContext {
private final TestContext testContext; private final TestContext testContext;
public RestDocumentationContext() { /**
this(null); * Creates a new {@code RestDocumentationContext} backed by the given
} * {@code testContext}.
*
* @param testContext the test context
*/
public RestDocumentationContext(TestContext testContext) { public RestDocumentationContext(TestContext testContext) {
this.testContext = testContext; this.testContext = testContext;
} }

View File

@@ -46,6 +46,12 @@ public class RestDocumentationContextPlaceholderResolver implements PlaceholderR
private final RestDocumentationContext context; private final RestDocumentationContext context;
/**
* Creates a new placeholder resolver that will resolve placeholders using the given
* {@code context}.
*
* @param context the context to use
*/
public RestDocumentationContextPlaceholderResolver(RestDocumentationContext context) { public RestDocumentationContextPlaceholderResolver(RestDocumentationContext context) {
this.context = context; this.context = context;
} }

View File

@@ -16,7 +16,13 @@
package org.springframework.restdocs.config; package org.springframework.restdocs.config;
import java.util.Arrays;
import java.util.List;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.restdocs.curl.CurlDocumentation;
import org.springframework.restdocs.http.HttpDocumentation;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.snippet.WriterResolver;
/** /**
@@ -28,6 +34,10 @@ import org.springframework.restdocs.snippet.WriterResolver;
public class SnippetConfigurer extends public class SnippetConfigurer extends
AbstractNestedConfigurer<RestDocumentationConfigurer> { AbstractNestedConfigurer<RestDocumentationConfigurer> {
private List<Snippet> defaultSnippets = Arrays.asList(
CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(),
HttpDocumentation.httpResponse());
/** /**
* The default encoding for documentation snippets * The default encoding for documentation snippets
* @see #withEncoding(String) * @see #withEncoding(String)
@@ -43,7 +53,7 @@ public class SnippetConfigurer extends
/** /**
* Configures any documentation snippets to be written using the given * Configures any documentation snippets to be written using the given
* {@code encoding}. The default is UTF-8. * {@code encoding}. The default is UTF-8.
* @param encoding The encoding * @param encoding the encoding
* @return {@code this} * @return {@code this}
*/ */
public SnippetConfigurer withEncoding(String encoding) { public SnippetConfigurer withEncoding(String encoding) {
@@ -55,5 +65,18 @@ public class SnippetConfigurer extends
void apply(MockHttpServletRequest request) { void apply(MockHttpServletRequest request) {
((WriterResolver) request.getAttribute(WriterResolver.class.getName())) ((WriterResolver) request.getAttribute(WriterResolver.class.getName()))
.setEncoding(this.snippetEncoding); .setEncoding(this.snippetEncoding);
request.setAttribute("org.springframework.restdocs.defaultSnippets",
this.defaultSnippets);
}
/**
* Configures the documentation snippets that will be produced by default.
*
* @param defaultSnippets the default snippets
* @return {@code this}
*/
public SnippetConfigurer withDefaults(Snippet... defaultSnippets) {
this.defaultSnippets = Arrays.asList(defaultSnippets);
return this;
} }
} }

View File

@@ -16,19 +16,9 @@
package org.springframework.restdocs.curl; package org.springframework.restdocs.curl;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import org.springframework.restdocs.snippet.DocumentableHttpServletRequest; import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
/** /**
* Static factory methods for documenting a RESTful API as if it were being driven using * Static factory methods for documenting a RESTful API as if it were being driven using
@@ -46,151 +36,25 @@ public abstract class CurlDocumentation {
} }
/** /**
* Produces a documentation snippet containing the request formatted as a cURL command * Returns a handler that will produce a snippet containing the curl request for the
* API call.
*
* @return the handler that will produce the snippet
*/
public static Snippet curlRequest() {
return new CurlRequestSnippet();
}
/**
* Returns a handler that will produce a snippet containing the curl request for the
* API call. The given {@code attributes} will be available during snippet generation.
* *
* @param identifier An identifier for the API call that is being documented
* @param attributes Attributes made available during rendering of the curl request * @param attributes Attributes made available during rendering of the curl request
* snippet * snippet
* @return the handler that will produce the snippet * @return the handler that will produce the snippet
*/ */
public static SnippetWritingResultHandler documentCurlRequest(String identifier, public static Snippet curlRequest(Map<String, Object> attributes) {
Map<String, Object> attributes) { return new CurlRequestSnippet(attributes);
return new CurlRequestWritingResultHandler(identifier, attributes);
}
private static final class CurlRequestWritingResultHandler extends
SnippetWritingResultHandler {
private static final String SCHEME_HTTP = "http";
private static final String SCHEME_HTTPS = "https";
private static final int STANDARD_PORT_HTTP = 80;
private static final int STANDARD_PORT_HTTPS = 443;
private CurlRequestWritingResultHandler(String identifier,
Map<String, Object> attributes) {
super(identifier, "curl-request", attributes);
}
@Override
public Map<String, Object> doHandle(MvcResult result) throws IOException {
Map<String, Object> model = new HashMap<String, Object>();
model.put("arguments", getCurlCommandArguments(result));
return model;
}
private String getCurlCommandArguments(MvcResult result) throws IOException {
StringWriter command = new StringWriter();
PrintWriter printer = new PrintWriter(command);
DocumentableHttpServletRequest request = new DocumentableHttpServletRequest(
result.getRequest());
printer.print("'");
writeAuthority(request, printer);
writePathAndQueryString(request, printer);
printer.print("'");
writeOptionToIncludeHeadersInOutput(printer);
writeHttpMethodIfNecessary(request, printer);
writeHeaders(request, printer);
if (request.isMultipartRequest()) {
writeParts(request, printer);
}
writeContent(request, printer);
return command.toString();
}
private void writeAuthority(DocumentableHttpServletRequest request,
PrintWriter writer) {
writer.print(String.format("%s://%s", request.getScheme(), request.getHost()));
if (isNonStandardPort(request)) {
writer.print(String.format(":%d", request.getPort()));
}
}
private boolean isNonStandardPort(DocumentableHttpServletRequest request) {
return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP)
|| (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS);
}
private void writePathAndQueryString(DocumentableHttpServletRequest request,
PrintWriter writer) {
if (StringUtils.hasText(request.getContextPath())) {
writer.print(String.format(
request.getContextPath().startsWith("/") ? "%s" : "/%s",
request.getContextPath()));
}
writer.print(request.getRequestUriWithQueryString());
}
private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) {
writer.print(" -i");
}
private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request,
PrintWriter writer) {
if (!request.isGetRequest()) {
writer.print(String.format(" -X %s", request.getMethod()));
}
}
private void writeHeaders(DocumentableHttpServletRequest request,
PrintWriter writer) {
for (Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
for (String header : entry.getValue()) {
writer.print(String.format(" -H '%s: %s'", entry.getKey(), header));
}
}
}
private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer)
throws IOException {
for (Entry<String, List<MultipartFile>> entry : request.getMultipartFiles()
.entrySet()) {
for (MultipartFile file : entry.getValue()) {
writer.printf(" -F '%s=", file.getName());
if (!StringUtils.hasText(file.getOriginalFilename())) {
writer.append(new String(file.getBytes()));
}
else {
writer.printf("@%s", file.getOriginalFilename());
}
if (StringUtils.hasText(file.getContentType())) {
writer.append(";type=").append(file.getContentType());
}
writer.append("'");
}
}
}
private void writeContent(DocumentableHttpServletRequest request,
PrintWriter writer) throws IOException {
if (request.getContentLength() > 0) {
writer.print(String.format(" -d '%s'", request.getContentAsString()));
}
else if (request.isMultipartRequest()) {
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
for (String value : entry.getValue()) {
writer.print(String.format(" -F '%s=%s'", entry.getKey(), value));
}
}
}
else if (request.isPostRequest() || request.isPutRequest()) {
String queryString = request.getParameterMapAsQueryString();
if (StringUtils.hasText(queryString)) {
writer.print(String.format(" -d '%s'", queryString));
}
}
}
} }
} }

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.curl;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.restdocs.snippet.DocumentableHttpServletRequest;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
/**
* A {@link Snippet} that documents the curl command for a request.
*
* @author Andy Wilkinson
*/
class CurlRequestSnippet extends TemplatedSnippet {
private static final String SCHEME_HTTP = "http";
private static final String SCHEME_HTTPS = "https";
private static final int STANDARD_PORT_HTTP = 80;
private static final int STANDARD_PORT_HTTPS = 443;
CurlRequestSnippet() {
this(null);
}
CurlRequestSnippet(Map<String, Object> attributes) {
super("curl-request", attributes);
}
@Override
public Map<String, Object> document(MvcResult result) throws IOException {
Map<String, Object> model = new HashMap<String, Object>();
model.put("arguments", getCurlCommandArguments(result));
return model;
}
private String getCurlCommandArguments(MvcResult result) throws IOException {
StringWriter command = new StringWriter();
PrintWriter printer = new PrintWriter(command);
DocumentableHttpServletRequest request = new DocumentableHttpServletRequest(
result.getRequest());
printer.print("'");
writeAuthority(request, printer);
writePathAndQueryString(request, printer);
printer.print("'");
writeOptionToIncludeHeadersInOutput(printer);
writeHttpMethodIfNecessary(request, printer);
writeHeaders(request, printer);
if (request.isMultipartRequest()) {
writeParts(request, printer);
}
writeContent(request, printer);
return command.toString();
}
private void writeAuthority(DocumentableHttpServletRequest request, PrintWriter writer) {
writer.print(String.format("%s://%s", request.getScheme(), request.getHost()));
if (isNonStandardPort(request)) {
writer.print(String.format(":%d", request.getPort()));
}
}
private boolean isNonStandardPort(DocumentableHttpServletRequest request) {
return (SCHEME_HTTP.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTP)
|| (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS);
}
private void writePathAndQueryString(DocumentableHttpServletRequest request,
PrintWriter writer) {
if (StringUtils.hasText(request.getContextPath())) {
writer.print(String.format(request.getContextPath().startsWith("/") ? "%s"
: "/%s", request.getContextPath()));
}
writer.print(request.getRequestUriWithQueryString());
}
private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) {
writer.print(" -i");
}
private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request,
PrintWriter writer) {
if (!request.isGetRequest()) {
writer.print(String.format(" -X %s", request.getMethod()));
}
}
private void writeHeaders(DocumentableHttpServletRequest request, PrintWriter writer) {
for (Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
for (String header : entry.getValue()) {
writer.print(String.format(" -H '%s: %s'", entry.getKey(), header));
}
}
}
private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer)
throws IOException {
for (Entry<String, List<MultipartFile>> entry : request.getMultipartFiles()
.entrySet()) {
for (MultipartFile file : entry.getValue()) {
writer.printf(" -F '%s=", file.getName());
if (!StringUtils.hasText(file.getOriginalFilename())) {
writer.append(new String(file.getBytes()));
}
else {
writer.printf("@%s", file.getOriginalFilename());
}
if (StringUtils.hasText(file.getContentType())) {
writer.append(";type=").append(file.getContentType());
}
writer.append("'");
}
}
}
private void writeContent(DocumentableHttpServletRequest request, PrintWriter writer)
throws IOException {
if (request.getContentLength() > 0) {
writer.print(String.format(" -d '%s'", request.getContentAsString()));
}
else if (request.isMultipartRequest()) {
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
for (String value : entry.getValue()) {
writer.print(String.format(" -F '%s=%s'", entry.getKey(), value));
}
}
}
else if (request.isPostRequest() || request.isPutRequest()) {
String queryString = request.getParameterMapAsQueryString();
if (StringUtils.hasText(queryString)) {
writer.print(String.format(" -d '%s'", queryString));
}
}
}
}

View File

@@ -16,23 +16,9 @@
package org.springframework.restdocs.http; package org.springframework.restdocs.http;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.HttpHeaders; import org.springframework.restdocs.snippet.Snippet;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.restdocs.snippet.DocumentableHttpServletRequest;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
/** /**
* Static factory methods for documenting a RESTful API's HTTP requests. * Static factory methods for documenting a RESTful API's HTTP requests.
@@ -42,213 +28,49 @@ import org.springframework.web.multipart.MultipartFile;
*/ */
public abstract class HttpDocumentation { public abstract class HttpDocumentation {
private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
private HttpDocumentation() { private HttpDocumentation() {
} }
/** /**
* Produces a documentation snippet containing the request formatted as an HTTP * Returns a handler that will produce a snippet containing the HTTP request for the
* request * API call.
* *
* @param identifier An identifier for the API call that is being documented
* @param attributes Attributes made available during rendering of the HTTP requst
* snippet
* @return the handler that will produce the snippet * @return the handler that will produce the snippet
*/ */
public static SnippetWritingResultHandler documentHttpRequest(String identifier, public static Snippet httpRequest() {
Map<String, Object> attributes) { return new HttpRequestSnippet();
return new HttpRequestWritingResultHandler(identifier, attributes);
} }
/** /**
* Produces a documentation snippet containing the response formatted as the HTTP * Returns a handler that will produce a snippet containing the HTTP request for the
* response sent by the server * API call. The given {@code attributes} will be available during snippet generation.
* *
* @param identifier An identifier for the API call that is being documented * @param attributes the attributes
* @param attributes Attributes made available during rendering of the HTTP response
* snippet
* @return the handler that will produce the snippet * @return the handler that will produce the snippet
*/ */
public static SnippetWritingResultHandler documentHttpResponse(String identifier, public static Snippet httpRequest(Map<String, Object> attributes) {
Map<String, Object> attributes) { return new HttpRequestSnippet(attributes);
return new HttpResponseWritingResultHandler(identifier, attributes);
} }
private static final class HttpRequestWritingResultHandler extends /**
SnippetWritingResultHandler { * Returns a handler that will produce a snippet containing the HTTP response for the
* API call.
private HttpRequestWritingResultHandler(String identifier, * @return the handler that will produce the snippet
Map<String, Object> attributes) { */
super(identifier, "http-request", attributes); public static Snippet httpResponse() {
} return new HttpResponseSnippet();
@Override
public Map<String, Object> doHandle(MvcResult result) throws IOException {
DocumentableHttpServletRequest request = new DocumentableHttpServletRequest(
result.getRequest());
Map<String, Object> model = new HashMap<String, Object>();
model.put("method", result.getRequest().getMethod());
model.put("path", request.getRequestUriWithQueryString());
model.put("headers", getHeaders(request));
model.put("requestBody", getRequestBody(request));
return model;
}
private List<Map<String, String>> getHeaders(
DocumentableHttpServletRequest request) {
List<Map<String, String>> headers = new ArrayList<>();
if (requiresHostHeader(request)) {
headers.add(header(HttpHeaders.HOST, request.getHost()));
}
for (Entry<String, List<String>> header : request.getHeaders().entrySet()) {
for (String value : header.getValue()) {
if (header.getKey() == HttpHeaders.CONTENT_TYPE
&& request.isMultipartRequest()) {
headers.add(header(header.getKey(), String.format(
"%s; boundary=%s", value, MULTIPART_BOUNDARY)));
}
else {
headers.add(header(header.getKey(), value));
}
}
}
if (requiresFormEncodingContentType(request)) {
headers.add(header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_FORM_URLENCODED_VALUE));
}
return headers;
}
private String getRequestBody(DocumentableHttpServletRequest request)
throws IOException {
StringWriter httpRequest = new StringWriter();
PrintWriter writer = new PrintWriter(httpRequest);
if (request.getContentLength() > 0) {
writer.println();
writer.print(request.getContentAsString());
}
else if (request.isPostRequest() || request.isPutRequest()) {
if (request.isMultipartRequest()) {
writeParts(request, writer);
}
else {
String queryString = request.getParameterMapAsQueryString();
if (StringUtils.hasText(queryString)) {
writer.println();
writer.print(queryString);
}
}
}
return httpRequest.toString();
}
private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer)
throws IOException {
writer.println();
for (Entry<String, String[]> parameter : request.getParameterMap().entrySet()) {
for (String value : parameter.getValue()) {
writePartBoundary(writer);
writePart(parameter.getKey(), value, null, writer);
writer.println();
}
}
for (Entry<String, List<MultipartFile>> entry : request.getMultipartFiles()
.entrySet()) {
for (MultipartFile file : entry.getValue()) {
writePartBoundary(writer);
writePart(file, writer);
writer.println();
}
}
writeMultipartEnd(writer);
}
private void writePartBoundary(PrintWriter writer) {
writer.printf("--%s%n", MULTIPART_BOUNDARY);
}
private void writePart(String name, String value, String contentType,
PrintWriter writer) {
writer.printf("Content-Disposition: form-data; name=%s%n", name);
if (StringUtils.hasText(contentType)) {
writer.printf("Content-Type: %s%n", contentType);
}
writer.println();
writer.print(value);
}
private void writePart(MultipartFile part, PrintWriter writer) throws IOException {
writePart(part.getName(), new String(part.getBytes()), part.getContentType(),
writer);
}
private void writeMultipartEnd(PrintWriter writer) {
writer.printf("--%s--", MULTIPART_BOUNDARY);
}
private boolean requiresHostHeader(DocumentableHttpServletRequest request) {
return request.getHeaders().get(HttpHeaders.HOST) == null;
}
private boolean requiresFormEncodingContentType(
DocumentableHttpServletRequest request) {
return request.getHeaders().getContentType() == null
&& (request.isPostRequest() || request.isPutRequest())
&& StringUtils.hasText(request.getParameterMapAsQueryString());
}
private Map<String, String> header(String name, String value) {
Map<String, String> header = new HashMap<>();
header.put("name", name);
header.put("value", value);
return header;
}
} }
private static final class HttpResponseWritingResultHandler extends /**
SnippetWritingResultHandler { * Returns a handler that will produce a snippet containing the HTTP response for the
* API call. The given {@code attributes} will be available during snippet generation.
private HttpResponseWritingResultHandler(String identifier, *
Map<String, Object> attributes) { * @param attributes the attributes
super(identifier, "http-response", attributes); * @return the handler that will produce the snippet
} */
public static Snippet httpResponse(Map<String, Object> attributes) {
@Override return new HttpResponseSnippet(attributes);
public Map<String, Object> doHandle(MvcResult result) throws IOException {
HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus());
Map<String, Object> model = new HashMap<String, Object>();
model.put(
"responseBody",
StringUtils.hasLength(result.getResponse().getContentAsString()) ? String
.format("%n%s", result.getResponse().getContentAsString())
: "");
model.put("statusCode", status.value());
model.put("statusReason", status.getReasonPhrase());
model.put("headers", headers(result));
return model;
}
private List<Map<String, String>> headers(MvcResult result) {
List<Map<String, String>> headers = new ArrayList<>();
for (String headerName : result.getResponse().getHeaderNames()) {
for (String header : result.getResponse().getHeaders(headerName)) {
headers.add(header(headerName, header));
}
}
return headers;
}
private Map<String, String> header(String name, String value) {
Map<String, String> header = new HashMap<>();
header.put("name", name);
header.put("value", value);
return header;
}
} }
} }

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.http;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.restdocs.snippet.DocumentableHttpServletRequest;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
/**
* A {@link Snippet} that documents an HTTP request.
*
* @author Andy Wilkinson
*/
class HttpRequestSnippet extends TemplatedSnippet {
private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
HttpRequestSnippet() {
this(null);
}
HttpRequestSnippet(Map<String, Object> attributes) {
super("http-request", attributes);
}
@Override
public Map<String, Object> document(MvcResult result) throws IOException {
DocumentableHttpServletRequest request = new DocumentableHttpServletRequest(
result.getRequest());
Map<String, Object> model = new HashMap<String, Object>();
model.put("method", result.getRequest().getMethod());
model.put("path", request.getRequestUriWithQueryString());
model.put("headers", getHeaders(request));
model.put("requestBody", getRequestBody(request));
return model;
}
private List<Map<String, String>> getHeaders(DocumentableHttpServletRequest request) {
List<Map<String, String>> headers = new ArrayList<>();
if (requiresHostHeader(request)) {
headers.add(header(HttpHeaders.HOST, request.getHost()));
}
for (Entry<String, List<String>> header : request.getHeaders().entrySet()) {
for (String value : header.getValue()) {
if (header.getKey() == HttpHeaders.CONTENT_TYPE
&& request.isMultipartRequest()) {
headers.add(header(header.getKey(),
String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY)));
}
else {
headers.add(header(header.getKey(), value));
}
}
}
if (requiresFormEncodingContentType(request)) {
headers.add(header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_FORM_URLENCODED_VALUE));
}
return headers;
}
private String getRequestBody(DocumentableHttpServletRequest request)
throws IOException {
StringWriter httpRequest = new StringWriter();
PrintWriter writer = new PrintWriter(httpRequest);
if (request.getContentLength() > 0) {
writer.println();
writer.print(request.getContentAsString());
}
else if (request.isPostRequest() || request.isPutRequest()) {
if (request.isMultipartRequest()) {
writeParts(request, writer);
}
else {
String queryString = request.getParameterMapAsQueryString();
if (StringUtils.hasText(queryString)) {
writer.println();
writer.print(queryString);
}
}
}
return httpRequest.toString();
}
private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer)
throws IOException {
writer.println();
for (Entry<String, String[]> parameter : request.getParameterMap().entrySet()) {
for (String value : parameter.getValue()) {
writePartBoundary(writer);
writePart(parameter.getKey(), value, null, writer);
writer.println();
}
}
for (Entry<String, List<MultipartFile>> entry : request.getMultipartFiles()
.entrySet()) {
for (MultipartFile file : entry.getValue()) {
writePartBoundary(writer);
writePart(file, writer);
writer.println();
}
}
writeMultipartEnd(writer);
}
private void writePartBoundary(PrintWriter writer) {
writer.printf("--%s%n", MULTIPART_BOUNDARY);
}
private void writePart(String name, String value, String contentType,
PrintWriter writer) {
writer.printf("Content-Disposition: form-data; name=%s%n", name);
if (StringUtils.hasText(contentType)) {
writer.printf("Content-Type: %s%n", contentType);
}
writer.println();
writer.print(value);
}
private void writePart(MultipartFile part, PrintWriter writer) throws IOException {
writePart(part.getName(), new String(part.getBytes()), part.getContentType(),
writer);
}
private void writeMultipartEnd(PrintWriter writer) {
writer.printf("--%s--", MULTIPART_BOUNDARY);
}
private boolean requiresHostHeader(DocumentableHttpServletRequest request) {
return request.getHeaders().get(HttpHeaders.HOST) == null;
}
private boolean requiresFormEncodingContentType(DocumentableHttpServletRequest request) {
return request.getHeaders().getContentType() == null
&& (request.isPostRequest() || request.isPutRequest())
&& StringUtils.hasText(request.getParameterMapAsQueryString());
}
private Map<String, String> header(String name, String value) {
Map<String, String> header = new HashMap<>();
header.put("name", name);
header.put("value", value);
return header;
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.http;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
/**
* A {@link Snippet} that documents an HTTP response.
*
* @author Andy Wilkinson
*/
class HttpResponseSnippet extends TemplatedSnippet {
HttpResponseSnippet() {
this(null);
}
HttpResponseSnippet(Map<String, Object> attributes) {
super("http-response", attributes);
}
@Override
public Map<String, Object> document(MvcResult result) throws IOException {
HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus());
Map<String, Object> model = new HashMap<String, Object>();
model.put(
"responseBody",
StringUtils.hasLength(result.getResponse().getContentAsString()) ? String
.format("%n%s", result.getResponse().getContentAsString()) : "");
model.put("statusCode", status.value());
model.put("statusReason", status.getReasonPhrase());
model.put("headers", headers(result));
return model;
}
private List<Map<String, String>> headers(MvcResult result) {
List<Map<String, String>> headers = new ArrayList<>();
for (String headerName : result.getResponse().getHeaderNames()) {
for (String header : result.getResponse().getHeaders(headerName)) {
headers.add(header(headerName, header));
}
}
return headers;
}
private Map<String, String> header(String name, String value) {
Map<String, String> header = new HashMap<>();
header.put("name", name);
header.put("value", value);
return header;
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.hypermedia;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.springframework.mock.web.MockHttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Abstract base class for a {@link LinkExtractor} that extracts links from JSON
*
* @author Andy Wilkinson
*/
abstract class AbstractJsonLinkExtractor implements LinkExtractor {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
@SuppressWarnings("unchecked")
public Map<String, List<Link>> extractLinks(MockHttpServletResponse response)
throws IOException {
Map<String, Object> jsonContent = this.objectMapper.readValue(
response.getContentAsString(), Map.class);
return extractLinks(jsonContent);
}
protected abstract Map<String, List<Link>> extractLinks(Map<String, Object> json);
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.hypermedia;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* {@link LinkExtractor} that extracts links in Atom format.
*
* @author Andy Wilkinson
*/
@SuppressWarnings("unchecked")
class AtomLinkExtractor extends AbstractJsonLinkExtractor {
@Override
public Map<String, List<Link>> extractLinks(Map<String, Object> json) {
MultiValueMap<String, Link> extractedLinks = new LinkedMultiValueMap<>();
Object possibleLinks = json.get("links");
if (possibleLinks instanceof Collection) {
Collection<Object> linksCollection = (Collection<Object>) possibleLinks;
for (Object linkObject : linksCollection) {
if (linkObject instanceof Map) {
Link link = maybeCreateLink((Map<String, Object>) linkObject);
maybeStoreLink(link, extractedLinks);
}
}
}
return extractedLinks;
}
private static Link maybeCreateLink(Map<String, Object> linkMap) {
Object hrefObject = linkMap.get("href");
Object relObject = linkMap.get("rel");
if (relObject instanceof String && hrefObject instanceof String) {
return new Link((String) relObject, (String) hrefObject);
}
return null;
}
private static void maybeStoreLink(Link link,
MultiValueMap<String, Link> extractedLinks) {
if (link != null) {
extractedLinks.add(link.getRel(), link);
}
}
}

View File

@@ -0,0 +1,58 @@
package org.springframework.restdocs.hypermedia;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.StringUtils;
/**
* {@link LinkExtractor} that delegates to other link extractors based on the response's
* content type.
*
* @author Andy Wilkinson
*
*/
class ContentTypeLinkExtractor implements LinkExtractor {
private Map<MediaType, LinkExtractor> linkExtractors = new HashMap<>();
ContentTypeLinkExtractor() {
this.linkExtractors.put(MediaType.APPLICATION_JSON, new AtomLinkExtractor());
this.linkExtractors.put(HalLinkExtractor.HAL_MEDIA_TYPE, new HalLinkExtractor());
}
ContentTypeLinkExtractor(Map<MediaType, LinkExtractor> linkExtractors) {
this.linkExtractors.putAll(linkExtractors);
}
@Override
public Map<String, List<Link>> extractLinks(MockHttpServletResponse response)
throws IOException {
String contentType = response.getContentType();
LinkExtractor extractorForContentType = getExtractorForContentType(contentType);
if (extractorForContentType != null) {
return extractorForContentType.extractLinks(response);
}
throw new IllegalStateException(
"No LinkExtractor has been provided and one is not available for the "
+ "content type " + contentType);
}
private LinkExtractor getExtractorForContentType(String contentType) {
if (StringUtils.hasText(contentType)) {
MediaType mediaType = MediaType.parseMediaType(contentType);
for (Entry<MediaType, LinkExtractor> entry : this.linkExtractors.entrySet()) {
if (mediaType.isCompatibleWith(entry.getKey())) {
return entry.getValue();
}
}
}
return null;
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.hypermedia;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.MediaType;
/**
* {@link LinkExtractor} that extracts links in Hypermedia Application Language (HAL)
* format.
*
* @author Andy Wilkinson
*/
class HalLinkExtractor extends AbstractJsonLinkExtractor {
static final MediaType HAL_MEDIA_TYPE = new MediaType("application", "hal+json");
@Override
public Map<String, List<Link>> extractLinks(Map<String, Object> json) {
Map<String, List<Link>> extractedLinks = new LinkedHashMap<>();
Object possibleLinks = json.get("_links");
if (possibleLinks instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> links = (Map<String, Object>) possibleLinks;
for (Entry<String, Object> entry : links.entrySet()) {
String rel = entry.getKey();
extractedLinks.put(rel, convertToLinks(entry.getValue(), rel));
}
}
return extractedLinks;
}
private static List<Link> convertToLinks(Object object, String rel) {
List<Link> links = new ArrayList<>();
if (object instanceof Collection) {
@SuppressWarnings("unchecked")
Collection<Object> hrefObjects = (Collection<Object>) object;
for (Object hrefObject : hrefObjects) {
maybeAddLink(maybeCreateLink(rel, hrefObject), links);
}
}
else {
maybeAddLink(maybeCreateLink(rel, object), links);
}
return links;
}
private static Link maybeCreateLink(String rel, Object possibleHref) {
if (possibleHref instanceof String) {
return new Link(rel, (String) possibleHref);
}
return null;
}
private static void maybeAddLink(Link possibleLink, List<Link> links) {
if (possibleLink != null) {
links.add(possibleLink);
}
}
}

View File

@@ -19,10 +19,10 @@ package org.springframework.restdocs.hypermedia;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import org.springframework.restdocs.RestDocumentationResultHandler; import org.springframework.restdocs.snippet.Snippet;
/** /**
* Static factory methods for documenting a RESTful API that utilises Hypermedia. * Static factory methods for documenting a RESTful API that utilizes Hypermedia.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
@@ -37,29 +37,89 @@ public abstract class HypermediaDocumentation {
* *
* @param rel The rel of the link * @param rel The rel of the link
* @return a {@code LinkDescriptor} ready for further configuration * @return a {@code LinkDescriptor} ready for further configuration
* @see RestDocumentationResultHandler#withLinks(LinkDescriptor...)
* @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...)
*/ */
public static LinkDescriptor linkWithRel(String rel) { public static LinkDescriptor linkWithRel(String rel) {
return new LinkDescriptor(rel); return new LinkDescriptor(rel);
} }
/** /**
* Creates a {@code LinkSnippetResultHandler} that will produce a documentation * Returns a handler that will produce a snippet documenting the links in the API
* snippet for a response's links. * call's response. Links will be extracted from the response automatically based on
* its content type.
*
* @param descriptors The descriptions of the response's links
* @return the handler
*/
public static Snippet links(LinkDescriptor... descriptors) {
return new LinksSnippet(new ContentTypeLinkExtractor(),
Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the links in the API
* call's response. The given {@code attributes} will be available during snippet
* generation. Links will be extracted from the response automatically based on its
* content type.
*
* @param attributes Attributes made available during rendering of the links snippet
* @param descriptors The descriptions of the response's links
* @return the handler
*/
public static Snippet links(Map<String, Object> attributes,
LinkDescriptor... descriptors) {
return new LinksSnippet(new ContentTypeLinkExtractor(), attributes,
Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the links in the API
* call's response. Links will be extracted from the response using the given
* {@code linkExtractor}.
*
* @param linkExtractor Used to extract the links from the response
* @param descriptors The descriptions of the response's links
* @return the handler
*/
public static Snippet links(LinkExtractor linkExtractor,
LinkDescriptor... descriptors) {
return new LinksSnippet(linkExtractor, Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the links in the API
* call's response. The given {@code attributes} will be available during snippet
* generation. Links will be extracted from the response using the given
* {@code linkExtractor}.
* *
* @param identifier An identifier for the API call that is being documented
* @param attributes Attributes made available during rendering of the links snippet * @param attributes Attributes made available during rendering of the links snippet
* @param linkExtractor Used to extract the links from the response * @param linkExtractor Used to extract the links from the response
* @param descriptors The descriptions of the response's links * @param descriptors The descriptions of the response's links
* @return the handler * @return the handler
* @see RestDocumentationResultHandler#withLinks(LinkDescriptor...)
* @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...)
*/ */
public static LinkSnippetResultHandler documentLinks(String identifier, public static Snippet links(LinkExtractor linkExtractor,
Map<String, Object> attributes, LinkExtractor linkExtractor, Map<String, Object> attributes, LinkDescriptor... descriptors) {
LinkDescriptor... descriptors) { return new LinksSnippet(linkExtractor, attributes,
return new LinkSnippetResultHandler(identifier, attributes, linkExtractor,
Arrays.asList(descriptors)); Arrays.asList(descriptors));
} }
/**
* Returns a {@code LinkExtractor} capable of extracting links in Hypermedia
* Application Language (HAL) format where the links are found in a map named
* {@code _links}.
*
* @return The extract for HAL-style links
*/
public static LinkExtractor halLinks() {
return new HalLinkExtractor();
}
/**
* Returns a {@code LinkExtractor} capable of extracting links in Atom format where
* the links are found in an array named {@code links}.
*
* @return The extractor for Atom-style links
*/
public static LinkExtractor atomLinks() {
return new AtomLinkExtractor();
}
} }

View File

@@ -1,187 +0,0 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.hypermedia;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Static factory methods providing a selection of {@link LinkExtractor link extractors}
* for use when documentating a hypermedia-based API.
*
* @author Andy Wilkinson
*/
public abstract class LinkExtractors {
private LinkExtractors() {
}
/**
* Returns a {@code LinkExtractor} capable of extracting links in Hypermedia
* Application Language (HAL) format where the links are found in a map named
* {@code _links}.
*
* @return The extract for HAL-style links
*/
public static LinkExtractor halLinks() {
return new HalLinkExtractor();
}
/**
* Returns a {@code LinkExtractor} capable of extracting links in Atom format where
* the links are found in an array named {@code links}.
*
* @return The extractor for Atom-style links
*/
public static LinkExtractor atomLinks() {
return new AtomLinkExtractor();
}
/**
* Returns the {@code LinkExtractor} for the given {@code contentType} or {@code null}
* if there is no extractor for the content type.
*
* @param contentType The content type, may include parameters
* @return The extractor for the content type, or {@code null}
*/
public static LinkExtractor extractorForContentType(String contentType) {
if (StringUtils.hasText(contentType)) {
MediaType mediaType = MediaType.parseMediaType(contentType);
if (mediaType.isCompatibleWith(MediaType.APPLICATION_JSON)) {
return atomLinks();
}
if (mediaType.isCompatibleWith(HalLinkExtractor.HAL_MEDIA_TYPE)) {
return halLinks();
}
}
return null;
}
private abstract static class JsonContentLinkExtractor implements LinkExtractor {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
@SuppressWarnings("unchecked")
public Map<String, List<Link>> extractLinks(MockHttpServletResponse response)
throws IOException {
Map<String, Object> jsonContent = this.objectMapper.readValue(
response.getContentAsString(), Map.class);
return extractLinks(jsonContent);
}
protected abstract Map<String, List<Link>> extractLinks(Map<String, Object> json);
}
@SuppressWarnings("unchecked")
static class HalLinkExtractor extends JsonContentLinkExtractor {
private static final MediaType HAL_MEDIA_TYPE = new MediaType("application",
"hal+json");
@Override
public Map<String, List<Link>> extractLinks(Map<String, Object> json) {
Map<String, List<Link>> extractedLinks = new LinkedHashMap<>();
Object possibleLinks = json.get("_links");
if (possibleLinks instanceof Map) {
Map<String, Object> links = (Map<String, Object>) possibleLinks;
for (Entry<String, Object> entry : links.entrySet()) {
String rel = entry.getKey();
extractedLinks.put(rel, convertToLinks(entry.getValue(), rel));
}
}
return extractedLinks;
}
private static List<Link> convertToLinks(Object object, String rel) {
List<Link> links = new ArrayList<>();
if (object instanceof Collection) {
Collection<Object> hrefObjects = (Collection<Object>) object;
for (Object hrefObject : hrefObjects) {
maybeAddLink(maybeCreateLink(rel, hrefObject), links);
}
}
else {
maybeAddLink(maybeCreateLink(rel, object), links);
}
return links;
}
private static Link maybeCreateLink(String rel, Object possibleHref) {
if (possibleHref instanceof String) {
return new Link(rel, (String) possibleHref);
}
return null;
}
private static void maybeAddLink(Link possibleLink, List<Link> links) {
if (possibleLink != null) {
links.add(possibleLink);
}
}
}
@SuppressWarnings("unchecked")
static class AtomLinkExtractor extends JsonContentLinkExtractor {
@Override
public Map<String, List<Link>> extractLinks(Map<String, Object> json) {
MultiValueMap<String, Link> extractedLinks = new LinkedMultiValueMap<>();
Object possibleLinks = json.get("links");
if (possibleLinks instanceof Collection) {
Collection<Object> linksCollection = (Collection<Object>) possibleLinks;
for (Object linkObject : linksCollection) {
if (linkObject instanceof Map) {
Link link = maybeCreateLink((Map<String, Object>) linkObject);
maybeStoreLink(link, extractedLinks);
}
}
}
return extractedLinks;
}
private static Link maybeCreateLink(Map<String, Object> linkMap) {
Object hrefObject = linkMap.get("href");
Object relObject = linkMap.get("rel");
if (relObject instanceof String && hrefObject instanceof String) {
return new Link((String) relObject, (String) hrefObject);
}
return null;
}
private static void maybeStoreLink(Link link,
MultiValueMap<String, Link> extractedLinks) {
if (link != null) {
extractedLinks.add(link.getRel(), link);
}
}
}
}

View File

@@ -26,29 +26,33 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler; import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful * A {@link Snippet} that documents a RESTful resource's links.
* resource's links.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public class LinkSnippetResultHandler extends SnippetWritingResultHandler { class LinksSnippet extends TemplatedSnippet {
private final Map<String, LinkDescriptor> descriptorsByRel = new LinkedHashMap<>(); private final Map<String, LinkDescriptor> descriptorsByRel = new LinkedHashMap<>();
private final Set<String> requiredRels = new HashSet<>(); private final Set<String> requiredRels = new HashSet<>();
private final LinkExtractor extractor; private final LinkExtractor linkExtractor;
LinkSnippetResultHandler(String identifier, Map<String, Object> attributes, LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descriptors) {
LinkExtractor linkExtractor, List<LinkDescriptor> descriptors) { this(linkExtractor, null, descriptors);
super(identifier, "links", attributes); }
this.extractor = linkExtractor;
LinksSnippet(LinkExtractor linkExtractor, Map<String, Object> attributes,
List<LinkDescriptor> descriptors) {
super("links", attributes);
this.linkExtractor = linkExtractor;
for (LinkDescriptor descriptor : descriptors) { for (LinkDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getRel()); Assert.hasText(descriptor.getRel());
Assert.hasText(descriptor.getDescription()); Assert.hasText(descriptor.getDescription());
@@ -60,31 +64,13 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler {
} }
@Override @Override
protected Map<String, Object> doHandle(MvcResult result) throws IOException { protected Map<String, Object> document(MvcResult result) throws IOException {
validate(extractLinks(result)); validate(this.linkExtractor.extractLinks(result.getResponse()));
Map<String, Object> model = new HashMap<>(); Map<String, Object> model = new HashMap<>();
model.put("links", createLinksModel()); model.put("links", createLinksModel());
return model; return model;
} }
private Map<String, List<Link>> extractLinks(MvcResult result) throws IOException {
if (this.extractor != null) {
return this.extractor.extractLinks(result.getResponse());
}
else {
String contentType = result.getResponse().getContentType();
LinkExtractor extractorForContentType = LinkExtractors
.extractorForContentType(contentType);
if (extractorForContentType != null) {
return extractorForContentType.extractLinks(result.getResponse());
}
throw new IllegalStateException(
"No LinkExtractor has been provided and one is not available for the content type "
+ contentType);
}
}
private void validate(Map<String, List<Link>> links) { private void validate(Map<String, List<Link>> links) {
Set<String> actualRels = links.keySet(); Set<String> actualRels = links.keySet();
@@ -104,10 +90,10 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler {
if (message.length() > 0) { if (message.length() > 0) {
message += ". "; message += ". ";
} }
message += "Links with the following relations were not found in the response: " message += "Links with the following relations were not found in the "
+ missingRels; + "response: " + missingRels;
} }
throw new SnippetGenerationException(message); throw new SnippetException(message);
} }
} }

View File

@@ -25,20 +25,21 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler; import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
/** /**
* A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful * A {@link TemplatedSnippet} that produces a snippet documenting a
* resource's request or response fields. * RESTful resource's request or response fields.
* *
* @author Andreas Evers * @author Andreas Evers
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public abstract class FieldSnippetResultHandler extends SnippetWritingResultHandler { public abstract class AbstractFieldsSnippet extends
TemplatedSnippet {
private final Map<String, FieldDescriptor> descriptorsByPath = new LinkedHashMap<String, FieldDescriptor>(); private final Map<String, FieldDescriptor> descriptorsByPath = new LinkedHashMap<String, FieldDescriptor>();
@@ -50,9 +51,9 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
private List<FieldDescriptor> fieldDescriptors; private List<FieldDescriptor> fieldDescriptors;
FieldSnippetResultHandler(String identifier, String type, AbstractFieldsSnippet(String type, Map<String, Object> attributes,
Map<String, Object> attributes, List<FieldDescriptor> descriptors) { List<FieldDescriptor> descriptors) {
super(identifier, type + "-fields", attributes); super(type + "-fields", attributes);
for (FieldDescriptor descriptor : descriptors) { for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath()); Assert.notNull(descriptor.getPath());
Assert.hasText(descriptor.getDescription()); Assert.hasText(descriptor.getDescription());
@@ -62,7 +63,7 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
} }
@Override @Override
protected Map<String, Object> doHandle(MvcResult result) throws IOException { protected Map<String, Object> document(MvcResult result) throws IOException {
this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors); this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors);
Object payload = extractPayload(result); Object payload = extractPayload(result);
Map<String, Object> model = new HashMap<>(); Map<String, Object> model = new HashMap<>();
@@ -80,8 +81,8 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { private FieldType getFieldType(FieldDescriptor descriptor, Object payload) {
try { try {
return FieldSnippetResultHandler.this.fieldTypeResolver.resolveFieldType( return AbstractFieldsSnippet.this.fieldTypeResolver
descriptor.getPath(), payload); .resolveFieldType(descriptor.getPath(), payload);
} }
catch (FieldDoesNotExistException ex) { catch (FieldDoesNotExistException ex) {
String message = "Cannot determine the type of the field '" String message = "Cannot determine the type of the field '"

View File

@@ -22,7 +22,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
@@ -60,7 +60,7 @@ class FieldValidator {
message += "Fields with the following paths were not found in the payload: " message += "Fields with the following paths were not found in the payload: "
+ missingFields; + missingFields;
} }
throw new SnippetGenerationException(message); throw new SnippetException(message);
} }
} }

View File

@@ -19,7 +19,7 @@ package org.springframework.restdocs.payload;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
import org.springframework.restdocs.RestDocumentationResultHandler; import org.springframework.restdocs.snippet.Snippet;
/** /**
* Static factory methods for documenting a RESTful API's request and response payloads. * Static factory methods for documenting a RESTful API's request and response payloads.
@@ -89,57 +89,92 @@ public abstract class PayloadDocumentation {
* *
* @param path The path of the field * @param path The path of the field
* @return a {@code FieldDescriptor} ready for further configuration * @return a {@code FieldDescriptor} ready for further configuration
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
*/ */
public static FieldDescriptor fieldWithPath(String path) { public static FieldDescriptor fieldWithPath(String path) {
return new FieldDescriptor(path); return new FieldDescriptor(path);
} }
/** /**
* Creates a {@code RequestFieldsSnippetResultHandler} that will produce a * Returns a handler that will produce a snippet documenting the fields of the API
* documentation snippet for a request's fields. * call's request.
* <p> * <p>
* If a field is present in the request but is not documented by one of the * If a field is present in the request, but is not documented by one of the
* descriptors a failure will occur when the handler is invoked. Similarly, if a field * descriptors, a failure will occur when the handler is invoked. Similarly, if a
* is documented, is not marked as optional, and is not present in the request a * field is documented, is not marked as optional, and is not present in the request,
* failure will also occur. For payloads with a hierarchical structure, documenting a * a failure will also occur. For payloads with a hierarchical structure, documenting
* field is sufficient for all of its descendants to also be treated as having been * a field is sufficient for all of its descendants to also be treated as having been
* documented. * documented.
* *
* @param identifier An identifier for the API call that is being documented
* @param attributes Attributes made available during rendering of the links snippet
* @param descriptors The descriptions of the request's fields * @param descriptors The descriptions of the request's fields
* @return the handler * @return the handler
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
* @see #fieldWithPath(String) * @see #fieldWithPath(String)
*/ */
public static FieldSnippetResultHandler documentRequestFields(String identifier, public static Snippet requestFields(FieldDescriptor... descriptors) {
Map<String, Object> attributes, FieldDescriptor... descriptors) { return new RequestFieldsSnippet(Arrays.asList(descriptors));
return new RequestFieldSnippetResultHandler(identifier, attributes, }
/**
* Returns a handler that will produce a snippet documenting the fields of the API
* call's request. The given {@code attributes} will be available during snippet
* generation.
* <p>
* If a field is present in the request, but is not documented by one of the
* descriptors, a failure will occur when the handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the request,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param attributes Attributes made available during rendering of the snippet
* @param descriptors The descriptions of the request's fields
* @return the handler
* @see #fieldWithPath(String)
*/
public static Snippet requestFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
return new RequestFieldsSnippet(attributes,
Arrays.asList(descriptors)); Arrays.asList(descriptors));
} }
/** /**
* Creates a {@code ResponseFieldsSnippetResultHandler} that will produce a * Returns a handler that will produce a snippet documenting the fields of the API
* documentation snippet for a response's fields. * call's response.
* <p> * <p>
* If a field is present in the response but is not documented by one of the * If a field is present in the response, but is not documented by one of the
* descriptors a failure will occur when the handler is invoked. Similarly, if a field * descriptors, a failure will occur when the handler is invoked. Similarly, if a
* is documented, is not marked as optional, and is not present in the response a * field is documented, is not marked as optional, and is not present in the response,
* failure will also occur. For payloads with a hierarchical structure, documenting a * a failure will also occur. For payloads with a hierarchical structure, documenting
* field is sufficient for all of its descendants to also be treated as having been * a field is sufficient for all of its descendants to also be treated as having been
* documented. * documented.
* *
* @param identifier An identifier for the API call that is being documented
* @param attributes Attributes made available during rendering of the links snippet
* @param descriptors The descriptions of the response's fields * @param descriptors The descriptions of the response's fields
* @return the handler * @return the handler
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...) * @see #fieldWithPath(String)
*/ */
public static FieldSnippetResultHandler documentResponseFields(String identifier, public static Snippet responseFields(FieldDescriptor... descriptors) {
Map<String, Object> attributes, FieldDescriptor... descriptors) { return new ResponseFieldsSnippet(Arrays.asList(descriptors));
return new ResponseFieldSnippetResultHandler(identifier, attributes, }
/**
* Returns a handler that will produce a snippet documenting the fields of the API
* call's response. The given {@code attributes} will be available during snippet
* generation.
* <p>
* If a field is present in the response, but is not documented by one of the
* descriptors, a failure will occur when the handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the response,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param attributes Attributes made available during rendering of the snippet
* @param descriptors The descriptions of the response's fields
* @return the handler
* @see #fieldWithPath(String)
*/
public static Snippet responseFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
return new ResponseFieldsSnippet(attributes,
Arrays.asList(descriptors)); Arrays.asList(descriptors));
} }

View File

@@ -20,18 +20,22 @@ import java.io.Reader;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
/** /**
* A {@link FieldSnippetResultHandler} for documenting a request's fields * A {@link Snippet} that documents the fields in a request.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public class RequestFieldSnippetResultHandler extends FieldSnippetResultHandler { class RequestFieldsSnippet extends AbstractFieldsSnippet {
RequestFieldSnippetResultHandler(String identifier, Map<String, Object> attributes, RequestFieldsSnippet(List<FieldDescriptor> descriptors) {
List<FieldDescriptor> descriptors) { this(null, descriptors);
super(identifier, "request", attributes, descriptors); }
RequestFieldsSnippet(Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
super("request", attributes, descriptors);
} }
@Override @Override

View File

@@ -21,18 +21,23 @@ import java.io.StringReader;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
/** /**
* A {@link FieldSnippetResultHandler} for documenting a response's fields * A {@link Snippet} the documents the fields in a response.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public class ResponseFieldSnippetResultHandler extends FieldSnippetResultHandler { class ResponseFieldsSnippet extends AbstractFieldsSnippet {
ResponseFieldSnippetResultHandler(String identifier, Map<String, Object> attributes, ResponseFieldsSnippet(List<FieldDescriptor> descriptors) {
this(null, descriptors);
}
ResponseFieldsSnippet(Map<String, Object> attributes,
List<FieldDescriptor> descriptors) { List<FieldDescriptor> descriptors) {
super(identifier, "response", attributes, descriptors); super("response", attributes, descriptors);
} }
@Override @Override

View File

@@ -10,19 +10,18 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler; import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert; import org.springframework.util.Assert;
public abstract class AbstractParametersSnippetResultHandler extends abstract class AbstractParametersSnippet extends
SnippetWritingResultHandler { TemplatedSnippet {
private final Map<String, ParameterDescriptor> descriptorsByName = new LinkedHashMap<>(); private final Map<String, ParameterDescriptor> descriptorsByName = new LinkedHashMap<>();
protected AbstractParametersSnippetResultHandler(String identifier, protected AbstractParametersSnippet(String snippetName,
String snippetName, Map<String, Object> attributes, Map<String, Object> attributes, List<ParameterDescriptor> descriptors) {
ParameterDescriptor... descriptors) { super(snippetName, attributes);
super(identifier, snippetName, attributes);
for (ParameterDescriptor descriptor : descriptors) { for (ParameterDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getName()); Assert.hasText(descriptor.getName());
Assert.hasText(descriptor.getDescription()); Assert.hasText(descriptor.getDescription());
@@ -31,7 +30,7 @@ public abstract class AbstractParametersSnippetResultHandler extends
} }
@Override @Override
protected Map<String, Object> doHandle(MvcResult result) throws IOException { protected Map<String, Object> document(MvcResult result) throws IOException {
verifyParameterDescriptors(result); verifyParameterDescriptors(result);
Map<String, Object> model = new HashMap<>(); Map<String, Object> model = new HashMap<>();

View File

@@ -17,30 +17,33 @@
package org.springframework.restdocs.request; package org.springframework.restdocs.request;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler; import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
* A {@link SnippetWritingResultHandler} that produces a snippet documenting the path * A {@link Snippet} that documents the path parameters supported by a RESTful resource.
* parameters supported by a RESTful resource.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public class PathParametersSnippetResultHandler extends class PathParametersSnippet extends AbstractParametersSnippet {
AbstractParametersSnippetResultHandler {
private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
protected PathParametersSnippetResultHandler(String identifier, PathParametersSnippet(List<ParameterDescriptor> descriptors) {
Map<String, Object> attributes, ParameterDescriptor... descriptors) { this(null, descriptors);
super(identifier, "path-parameters", attributes, descriptors); }
PathParametersSnippet(Map<String, Object> attributes,
List<ParameterDescriptor> descriptors) {
super("path-parameters", attributes, descriptors);
} }
@Override @Override
@@ -84,7 +87,7 @@ public class PathParametersSnippetResultHandler extends
message += "Path parameters with the following names were not found in " message += "Path parameters with the following names were not found in "
+ "the request: " + missingParameters; + "the request: " + missingParameters;
} }
throw new SnippetGenerationException(message); throw new SnippetException(message);
} }
} }

View File

@@ -16,25 +16,28 @@
package org.springframework.restdocs.request; package org.springframework.restdocs.request;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler; import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
/** /**
* A {@link SnippetWritingResultHandler} that produces a snippet documenting the query * A {@link Snippet} that documents the query parameters supported by a RESTful resource.
* parameters supported by a RESTful resource.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public class QueryParametersSnippetResultHandler extends class QueryParametersSnippet extends AbstractParametersSnippet {
AbstractParametersSnippetResultHandler {
protected QueryParametersSnippetResultHandler(String identifier, QueryParametersSnippet(List<ParameterDescriptor> descriptors) {
Map<String, Object> attributes, ParameterDescriptor... descriptors) { this(null, descriptors);
super(identifier, "query-parameters", attributes, descriptors); }
QueryParametersSnippet(Map<String, Object> attributes,
List<ParameterDescriptor> descriptors) {
super("query-parameters", attributes, descriptors);
} }
@Override @Override
@@ -52,7 +55,7 @@ public class QueryParametersSnippetResultHandler extends
message += "Query parameters with the following names were not found in the request: " message += "Query parameters with the following names were not found in the request: "
+ missingParameters; + missingParameters;
} }
throw new SnippetGenerationException(message); throw new SnippetException(message);
} }
@Override @Override

View File

@@ -16,10 +16,10 @@
package org.springframework.restdocs.request; package org.springframework.restdocs.request;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
import org.springframework.restdocs.RestDocumentationResultHandler; import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
/** /**
* Static factory methods for documenting aspects of a request sent to a RESTful API. * Static factory methods for documenting aspects of a request sent to a RESTful API.
@@ -32,49 +32,69 @@ public abstract class RequestDocumentation {
} }
/**
* Creates a {@link SnippetWritingResultHandler} that will produce a snippet
* documenting a request's path parameters.
*
* @param identifier An identifier for the API call that is being documented
* @param attributes Attributes made available during rendering of the path parameters
* snippet
* @param descriptors The descriptions of the parameters in the request's path
* @return the result handler
* @see RestDocumentationResultHandler#withPathParameters(ParameterDescriptor...)
*/
public static SnippetWritingResultHandler documentPathParameters(String identifier,
Map<String, Object> attributes, ParameterDescriptor... descriptors) {
return new PathParametersSnippetResultHandler(identifier, attributes, descriptors);
}
/**
* Creates a {@link SnippetWritingResultHandler} that will produce a snippet
* documenting a request's query parameters
*
* @param identifier An identifier for the API call that is being documented
* @param attributes Attributes made available during rendering of the query
* parameters snippet
* @param descriptors The descriptions of the parameters in the request's query string
* @return the result handler
* @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...)
*/
public static SnippetWritingResultHandler documentQueryParameters(String identifier,
Map<String, Object> attributes, ParameterDescriptor... descriptors) {
return new QueryParametersSnippetResultHandler(identifier, attributes,
descriptors);
}
/** /**
* Creates a {@link ParameterDescriptor} that describes a query string parameter with * Creates a {@link ParameterDescriptor} that describes a query string parameter with
* the given {@code name}. * the given {@code name}.
* *
* @param name The name of the parameter * @param name The name of the parameter
* @return a {@link ParameterDescriptor} ready for further configuration * @return a {@link ParameterDescriptor} ready for further configuration
* @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...)
*/ */
public static ParameterDescriptor parameterWithName(String name) { public static ParameterDescriptor parameterWithName(String name) {
return new ParameterDescriptor(name); return new ParameterDescriptor(name);
} }
/**
* Returns a handler that will produce a snippet documenting the path parameters from
* the API call's request.
*
* @param descriptors The descriptions of the parameters in the request's path
* @return the handler
*/
public static Snippet pathParameters(ParameterDescriptor... descriptors) {
return new PathParametersSnippet(Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the path parameters from
* the API call's request. The given {@code attributes} will be available during
* snippet generation.
*
* @param attributes Attributes made available during rendering of the path parameters
* snippet
* @param descriptors The descriptions of the parameters in the request's path
* @return the handler
*/
public static Snippet pathParameters(Map<String, Object> attributes,
ParameterDescriptor... descriptors) {
return new PathParametersSnippet(attributes,
Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the query parameters from
* the API call's request.
*
* @param descriptors The descriptions of the request's query parameters
* @return the handler
*/
public static Snippet queryParameters(ParameterDescriptor... descriptors) {
return new QueryParametersSnippet(Arrays.asList(descriptors));
}
/**
* Returns a handler that will produce a snippet documenting the query parameters from
* the API call's request. The given {@code attributes} will be available during
* snippet generation.
*
* @param attributes Attributes made available during rendering of the query
* parameters snippet
* @param descriptors The descriptions of the request's query parameters
* @return the handler
*/
public static Snippet queryParameters(Map<String, Object> attributes,
ParameterDescriptor... descriptors) {
return new QueryParametersSnippet(attributes,
Arrays.asList(descriptors));
}
} }

View File

@@ -67,7 +67,7 @@ public abstract class ContentModifyingReponsePostProcessor implements
private final MockHttpServletResponse delegate; private final MockHttpServletResponse delegate;
public ContentModifyingMethodInterceptor(String modifiedContent, private ContentModifyingMethodInterceptor(String modifiedContent,
MockHttpServletResponse delegate) { MockHttpServletResponse delegate) {
this.modifiedContent = modifiedContent; this.modifiedContent = modifiedContent;
this.delegate = delegate; this.delegate = delegate;

View File

@@ -42,7 +42,7 @@ class HeaderRemovingResponsePostProcessor implements ResponsePostProcessor {
private final Set<String> headersToRemove; private final Set<String> headersToRemove;
public HeaderRemovingResponsePostProcessor(String... headersToRemove) { HeaderRemovingResponsePostProcessor(String... headersToRemove) {
this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove));
} }

View File

@@ -75,12 +75,11 @@ public abstract class Attributes {
public Attribute value(Object value) { public Attribute value(Object value) {
return new Attribute(this.key, value); return new Attribute(this.key, value);
} }
} }
/** /**
* An attribute (key-value pair). * An attribute (key-value pair).
*
* @author Andy Wilkinson
*/ */
public static class Attribute { public static class Attribute {
@@ -88,18 +87,32 @@ public abstract class Attributes {
private final Object value; private final Object value;
/**
* Creates a new attribute with the given {@code key} and {@code value}.
* @param key the key
* @param value the value
*/
public Attribute(String key, Object value) { public Attribute(String key, Object value) {
this.key = key; this.key = key;
this.value = value; this.value = value;
} }
/**
* Returns the attribute's key
* @return the key
*/
public String getKey() { public String getKey() {
return this.key; return this.key;
} }
/**
* Returns the attribute's value
* @return the value
*/
public Object getValue() { public Object getValue() {
return this.value; return this.value;
} }
} }
} }

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.snippet;
import java.io.IOException;
import org.springframework.test.web.servlet.MvcResult;
/**
* A {@link Snippet} is used to document aspects of a call to a RESTful API.
*
* @author Andy Wilkinson
*/
public interface Snippet {
/**
* Documents the call to the RESTful API described by the given {@code result}. The
* call is identified by the given {@code operation}.
*
* @param operation the API operation
* @param result the result of the operation
* @throws IOException if a failure occurs will documenting the result
*/
void document(String operation, MvcResult result) throws IOException;
}

View File

@@ -23,14 +23,13 @@ package org.springframework.restdocs.snippet;
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class SnippetGenerationException extends RuntimeException { public class SnippetException extends RuntimeException {
/** /**
* Creates a new {@code SnippetGenerationException} described by the given * Creates a new {@code SnippetException} described by the given {@code message}
* {@code message}
* @param message the message that describes the problem * @param message the message that describes the problem
*/ */
public SnippetGenerationException(String message) { public SnippetException(String message) {
super(message); super(message);
} }

View File

@@ -21,26 +21,23 @@ import java.io.Writer;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.springframework.restdocs.templates.Template;
import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
/** /**
* Base class for a {@link ResultHandler} that writes a documentation snippet * Base class for a {@link Snippet} that is produced using a {@link Template} and
* {@link TemplateEngine}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public abstract class SnippetWritingResultHandler implements ResultHandler { public abstract class TemplatedSnippet implements Snippet {
private final Map<String, Object> attributes = new HashMap<>(); private final Map<String, Object> attributes = new HashMap<>();
private final String identifier;
private final String snippetName; private final String snippetName;
protected SnippetWritingResultHandler(String identifier, String snippetName, protected TemplatedSnippet(String snippetName, Map<String, Object> attributes) {
Map<String, Object> attributes) {
this.identifier = identifier;
this.snippetName = snippetName; this.snippetName = snippetName;
if (attributes != null) { if (attributes != null) {
this.attributes.putAll(attributes); this.attributes.putAll(attributes);
@@ -48,11 +45,11 @@ public abstract class SnippetWritingResultHandler implements ResultHandler {
} }
@Override @Override
public void handle(MvcResult result) throws IOException { public void document(String operation, MvcResult result) throws IOException {
WriterResolver writerResolver = (WriterResolver) result.getRequest() WriterResolver writerResolver = (WriterResolver) result.getRequest()
.getAttribute(WriterResolver.class.getName()); .getAttribute(WriterResolver.class.getName());
try (Writer writer = writerResolver.resolve(this.identifier, this.snippetName)) { try (Writer writer = writerResolver.resolve(operation, this.snippetName)) {
Map<String, Object> model = doHandle(result); Map<String, Object> model = document(result);
model.putAll(this.attributes); model.putAll(this.attributes);
TemplateEngine templateEngine = (TemplateEngine) result.getRequest() TemplateEngine templateEngine = (TemplateEngine) result.getRequest()
.getAttribute(TemplateEngine.class.getName()); .getAttribute(TemplateEngine.class.getName());
@@ -60,6 +57,6 @@ public abstract class SnippetWritingResultHandler implements ResultHandler {
} }
} }
protected abstract Map<String, Object> doHandle(MvcResult result) throws IOException; protected abstract Map<String, Object> document(MvcResult result) throws IOException;
} }

View File

@@ -23,9 +23,15 @@ import static org.junit.Assert.assertTrue;
import static org.springframework.restdocs.RestDocumentation.document; import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.RestDocumentation.modifyResponseTo; import static org.springframework.restdocs.RestDocumentation.modifyResponseTo;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks; import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks;
import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent; import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent;
import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders; import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders;
@@ -112,14 +118,14 @@ public class RestDocumentationIntegrationTests {
} }
@Test @Test
public void links() throws Exception { public void linksSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build(); .apply(new RestDocumentationConfigurer()).build();
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("links").withLinks( .andDo(document("links",
linkWithRel("rel").description("The description"))); links(linkWithRel("rel").description("The description"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc", "http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -127,14 +133,14 @@ public class RestDocumentationIntegrationTests {
} }
@Test @Test
public void pathParameters() throws Exception { public void pathParametersSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build(); .apply(new RestDocumentationConfigurer()).build();
mockMvc.perform(get("{foo}", "/").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("{foo}", "/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("links").withPathParameters( .andDo(document("links", pathParameters(parameterWithName("foo")
parameterWithName("foo").description("The description"))); .description("The description"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc", "http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -142,14 +148,14 @@ public class RestDocumentationIntegrationTests {
} }
@Test @Test
public void queryParameters() throws Exception { public void queryParametersSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build(); .apply(new RestDocumentationConfigurer()).build();
mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("links").withQueryParameters( .andDo(document("links", queryParameters(parameterWithName("foo")
parameterWithName("foo").description("The description"))); .description("The description"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc", "http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -157,7 +163,7 @@ public class RestDocumentationIntegrationTests {
} }
@Test @Test
public void requestFields() throws Exception { public void requestFieldsSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build(); .apply(new RestDocumentationConfigurer()).build();
@@ -165,8 +171,8 @@ public class RestDocumentationIntegrationTests {
get("/").param("foo", "bar").content("{\"a\":\"alpha\"}") get("/").param("foo", "bar").content("{\"a\":\"alpha\"}")
.accept(MediaType.APPLICATION_JSON)) .accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("links").withRequestFields( .andDo(document("links",
fieldWithPath("a").description("The description"))); requestFields(fieldWithPath("a").description("The description"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc", "http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -174,15 +180,18 @@ public class RestDocumentationIntegrationTests {
} }
@Test @Test
public void responseFields() throws Exception { public void responseFieldsSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build(); .apply(new RestDocumentationConfigurer()).build();
mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andDo(document("links").withResponseFields( .andDo(document(
fieldWithPath("a").description("The description"), "links",
fieldWithPath("links").description("Links to other resources"))); responseFields(
fieldWithPath("a").description("The description"),
fieldWithPath("links").description(
"Links to other resources"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc", "http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -284,8 +293,10 @@ public class RestDocumentationIntegrationTests {
is(snippet().withContents(equalTo("Custom curl request")))); is(snippet().withContents(equalTo("Custom curl request"))));
mockMvc.perform(get("/")).andDo( mockMvc.perform(get("/")).andDo(
document("index").withCurlRequest( document(
attributes(key("title").value("Access the index using curl")))); "index",
curlRequest(attributes(key("title").value(
"Access the index using curl")))));
} }
private void assertExpectedSnippetFilesExist(File directory, String... snippets) { private void assertExpectedSnippetFilesExist(File directory, String... snippets) {

View File

@@ -43,7 +43,7 @@ public class RestDocumentationConfigurerTests {
private MockHttpServletRequest request = new MockHttpServletRequest(); private MockHttpServletRequest request = new MockHttpServletRequest();
private RestDocumentationContext context = new RestDocumentationContext(); private RestDocumentationContext context = new RestDocumentationContext(null);
@Before @Before
public void establishContext() { public void establishContext() {

View File

@@ -0,0 +1,6 @@
package org.springframework.restdocs.config;
public class RestDocumentationContexts {
}

View File

@@ -23,7 +23,6 @@ import static org.springframework.restdocs.RestDocumentationRequestBuilders.file
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.RestDocumentationRequestBuilders.put;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest;
import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock;
@@ -44,14 +43,14 @@ import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.ExpectedSnippet;
/** /**
* Tests for {@link CurlDocumentation} * Tests for {@link CurlRequestSnippet}
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Yann Le Guern * @author Yann Le Guern
* @author Dmitriy Mayboroda * @author Dmitriy Mayboroda
* @author Jonathan Pearlin * @author Jonathan Pearlin
*/ */
public class CurlDocumentationTests { public class CurlRequestDocumentationHandlerTests {
@Rule @Rule
public ExpectedSnippet snippet = new ExpectedSnippet(); public ExpectedSnippet snippet = new ExpectedSnippet();
@@ -63,14 +62,16 @@ public class CurlDocumentationTests {
public void getRequest() throws IOException { public void getRequest() throws IOException {
this.snippet.expectCurlRequest("get-request").withContents( this.snippet.expectCurlRequest("get-request").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); codeBlock("bash").content("$ curl 'http://localhost/foo' -i"));
documentCurlRequest("get-request", null).handle(result(get("/foo"))); new CurlRequestSnippet()
.document("get-request", result(get("/foo")));
} }
@Test @Test
public void nonGetRequest() throws IOException { public void nonGetRequest() throws IOException {
this.snippet.expectCurlRequest("non-get-request").withContents( this.snippet.expectCurlRequest("non-get-request").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST"));
documentCurlRequest("non-get-request", null).handle(result(post("/foo"))); new CurlRequestSnippet().document("non-get-request",
result(post("/foo")));
} }
@Test @Test
@@ -78,7 +79,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("request-with-content").withContents( this.snippet.expectCurlRequest("request-with-content").withContents(
codeBlock("bash") codeBlock("bash")
.content("$ curl 'http://localhost/foo' -i -d 'content'")); .content("$ curl 'http://localhost/foo' -i -d 'content'"));
documentCurlRequest("request-with-content", null).handle( new CurlRequestSnippet().document("request-with-content",
result(get("/foo").content("content"))); result(get("/foo").content("content")));
} }
@@ -88,7 +89,7 @@ public class CurlDocumentationTests {
.withContents( .withContents(
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo?param=value' -i")); "$ curl 'http://localhost/foo?param=value' -i"));
documentCurlRequest("request-with-query-string", null).handle( new CurlRequestSnippet().document("request-with-query-string",
result(get("/foo?param=value"))); result(get("/foo?param=value")));
} }
@@ -96,7 +97,7 @@ public class CurlDocumentationTests {
public void requestWithOneParameter() throws IOException { public void requestWithOneParameter() throws IOException {
this.snippet.expectCurlRequest("request-with-one-parameter").withContents( this.snippet.expectCurlRequest("request-with-one-parameter").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo?k1=v1' -i")); codeBlock("bash").content("$ curl 'http://localhost/foo?k1=v1' -i"));
documentCurlRequest("request-with-one-parameter", null).handle( new CurlRequestSnippet().document("request-with-one-parameter",
result(get("/foo").param("k1", "v1"))); result(get("/foo").param("k1", "v1")));
} }
@@ -105,9 +106,9 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents( this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents(
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i")); "$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i"));
documentCurlRequest("request-with-multiple-parameters", null).handle( new CurlRequestSnippet().document(
result(get("/foo").param("k1", "v1").param("k2", "v2") "request-with-multiple-parameters", result(get("/foo").param("k1", "v1")
.param("k1", "v1-bis"))); .param("k2", "v2").param("k1", "v1-bis")));
} }
@Test @Test
@@ -116,7 +117,8 @@ public class CurlDocumentationTests {
.withContents( .withContents(
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo?k1=foo+bar%26' -i")); "$ curl 'http://localhost/foo?k1=foo+bar%26' -i"));
documentCurlRequest("request-with-url-encoded-parameter", null).handle( new CurlRequestSnippet().document(
"request-with-url-encoded-parameter",
result(get("/foo").param("k1", "foo bar&"))); result(get("/foo").param("k1", "foo bar&")));
} }
@@ -125,7 +127,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents(
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'"));
documentCurlRequest("post-request-with-one-parameter", null).handle( new CurlRequestSnippet().document("post-request-with-one-parameter",
result(post("/foo").param("k1", "v1"))); result(post("/foo").param("k1", "v1")));
} }
@@ -136,7 +138,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST" "$ curl 'http://localhost/foo' -i -X POST"
+ " -d 'k1=v1&k1=v1-bis&k2=v2'")); + " -d 'k1=v1&k1=v1-bis&k2=v2'"));
documentCurlRequest("post-request-with-multiple-parameters", null).handle( new CurlRequestSnippet().document(
"post-request-with-multiple-parameters",
result(post("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); result(post("/foo").param("k1", "v1", "v1-bis").param("k2", "v2")));
} }
@@ -147,7 +150,8 @@ public class CurlDocumentationTests {
.withContents( .withContents(
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'"));
documentCurlRequest("post-request-with-url-encoded-parameter", null).handle( new CurlRequestSnippet().document(
"post-request-with-url-encoded-parameter",
result(post("/foo").param("k1", "a&b"))); result(post("/foo").param("k1", "a&b")));
} }
@@ -156,7 +160,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents(
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'"));
documentCurlRequest("put-request-with-one-parameter", null).handle( new CurlRequestSnippet().document("put-request-with-one-parameter",
result(put("/foo").param("k1", "v1"))); result(put("/foo").param("k1", "v1")));
} }
@@ -167,7 +171,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT" "$ curl 'http://localhost/foo' -i -X PUT"
+ " -d 'k1=v1&k1=v1-bis&k2=v2'")); + " -d 'k1=v1&k1=v1-bis&k2=v2'"));
documentCurlRequest("put-request-with-multiple-parameters", null).handle( new CurlRequestSnippet().document(
"put-request-with-multiple-parameters",
result(put("/foo").param("k1", "v1", "v1-bis").param("k2", "v2"))); result(put("/foo").param("k1", "v1", "v1-bis").param("k2", "v2")));
} }
@@ -177,7 +182,8 @@ public class CurlDocumentationTests {
.withContents( .withContents(
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'"));
documentCurlRequest("put-request-with-url-encoded-parameter", null).handle( new CurlRequestSnippet().document(
"put-request-with-url-encoded-parameter",
result(put("/foo").param("k1", "a&b"))); result(put("/foo").param("k1", "a&b")));
} }
@@ -187,7 +193,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content( codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i" "$ curl 'http://localhost/foo' -i"
+ " -H 'Content-Type: application/json' -H 'a: alpha'")); + " -H 'Content-Type: application/json' -H 'a: alpha'"));
documentCurlRequest("request-with-headers", null).handle( new CurlRequestSnippet().document(
"request-with-headers",
result(get("/foo").contentType(MediaType.APPLICATION_JSON).header("a", result(get("/foo").contentType(MediaType.APPLICATION_JSON).header("a",
"alpha"))); "alpha")));
} }
@@ -198,7 +205,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i")); codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(8080); request.setServerPort(8080);
documentCurlRequest("http-with-non-standard-port", null).handle(result(request)); new CurlRequestSnippet().document("http-with-non-standard-port",
result(request));
} }
@Test @Test
@@ -208,7 +216,8 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(443); request.setServerPort(443);
request.setScheme("https"); request.setScheme("https");
documentCurlRequest("https-with-standard-port", null).handle(result(request)); new CurlRequestSnippet().document("https-with-standard-port",
result(request));
} }
@Test @Test
@@ -218,7 +227,8 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(8443); request.setServerPort(8443);
request.setScheme("https"); request.setScheme("https");
documentCurlRequest("https-with-non-standard-port", null).handle(result(request)); new CurlRequestSnippet().document("https-with-non-standard-port",
result(request));
} }
@Test @Test
@@ -227,7 +237,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i")); codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com"); request.setServerName("api.example.com");
documentCurlRequest("request-with-custom-host", null).handle(result(request)); new CurlRequestSnippet().document("request-with-custom-host",
result(request));
} }
@Test @Test
@@ -239,8 +250,8 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com"); request.setServerName("api.example.com");
request.setContextPath("/v3"); request.setContextPath("/v3");
documentCurlRequest("request-with-custom-context-with-slash", null).handle( new CurlRequestSnippet().document(
result(request)); "request-with-custom-context-with-slash", result(request));
} }
@Test @Test
@@ -252,8 +263,8 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com"); request.setServerName("api.example.com");
request.setContextPath("v3"); request.setContextPath("v3");
documentCurlRequest("request-with-custom-context-without-slash", null).handle( new CurlRequestSnippet().document(
result(request)); "request-with-custom-context-without-slash", result(request));
} }
@Test @Test
@@ -265,7 +276,8 @@ public class CurlDocumentationTests {
.withContents(codeBlock("bash").content(expectedContent)); .withContents(codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("metadata", MockMultipartFile multipartFile = new MockMultipartFile("metadata",
"{\"description\": \"foo\"}".getBytes()); "{\"description\": \"foo\"}".getBytes());
documentCurlRequest("multipart-post-no-original-filename", null).handle( new CurlRequestSnippet().document(
"multipart-post-no-original-filename",
result(fileUpload("/upload").file(multipartFile))); result(fileUpload("/upload").file(multipartFile)));
} }
@@ -279,7 +291,8 @@ public class CurlDocumentationTests {
MockMultipartFile multipartFile = new MockMultipartFile("image", MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", MediaType.IMAGE_PNG_VALUE, "documents/images/example.png", MediaType.IMAGE_PNG_VALUE,
"bytes".getBytes()); "bytes".getBytes());
documentCurlRequest("multipart-post-with-content-type", null).handle( new CurlRequestSnippet().document(
"multipart-post-with-content-type",
result(fileUpload("/upload").file(multipartFile))); result(fileUpload("/upload").file(multipartFile)));
} }
@@ -292,7 +305,7 @@ public class CurlDocumentationTests {
codeBlock("bash").content(expectedContent)); codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image", MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "bytes".getBytes()); "documents/images/example.png", null, "bytes".getBytes());
documentCurlRequest("multipart-post", null).handle( new CurlRequestSnippet().document("multipart-post",
result(fileUpload("/upload").file(multipartFile))); result(fileUpload("/upload").file(multipartFile)));
} }
@@ -306,7 +319,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content(expectedContent)); codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image", MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "bytes".getBytes()); "documents/images/example.png", null, "bytes".getBytes());
documentCurlRequest("multipart-post", null).handle( new CurlRequestSnippet().document(
"multipart-post",
result(fileUpload("/upload").file(multipartFile) result(fileUpload("/upload").file(multipartFile)
.param("a", "apple", "avocado").param("b", "banana"))); .param("a", "apple", "avocado").param("b", "banana")));
} }
@@ -323,9 +337,8 @@ public class CurlDocumentationTests {
"src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver)); resolver));
documentCurlRequest("custom-attributes", new CurlRequestSnippet(attributes(key("title").value(
attributes(key("title").value("curl request title"))).handle( "curl request title"))).document("custom-attributes", result(request));
result(request));
} }
} }

View File

@@ -19,18 +19,13 @@ package org.springframework.restdocs.http;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload; import static org.springframework.restdocs.RestDocumentationRequestBuilders.fileUpload;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.put; import static org.springframework.restdocs.RestDocumentationRequestBuilders.put;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse;
import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest;
import static org.springframework.restdocs.test.SnippetMatchers.httpResponse;
import static org.springframework.restdocs.test.StubMvcResult.result; import static org.springframework.restdocs.test.StubMvcResult.result;
import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.GET;
import static org.springframework.web.bind.annotation.RequestMethod.POST; import static org.springframework.web.bind.annotation.RequestMethod.POST;
@@ -44,7 +39,6 @@ import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.TemplateResourceResolver;
@@ -52,12 +46,13 @@ import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.ExpectedSnippet;
/** /**
* Tests for {@link HttpDocumentation} * Tests for {@link HttpRequestSnippet}
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Jonathan Pearlin * @author Jonathan Pearlin
*
*/ */
public class HttpDocumentationTests { public class HttpRequestDocumentationHandlerTests {
private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
@@ -70,8 +65,8 @@ public class HttpDocumentationTests {
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header( httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header(
"Alpha", "a")); "Alpha", "a"));
documentHttpRequest("get-request", null).handle( new HttpRequestSnippet().document("get-request", result(get("/foo")
result(get("/foo").header("Alpha", "a"))); .header("Alpha", "a")));
} }
@Test @Test
@@ -79,7 +74,7 @@ public class HttpDocumentationTests {
this.snippet.expectHttpRequest("get-request-with-query-string").withContents( this.snippet.expectHttpRequest("get-request-with-query-string").withContents(
httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost"));
documentHttpRequest("get-request-with-query-string", null).handle( new HttpRequestSnippet().document("get-request-with-query-string",
result(get("/foo?bar=baz"))); result(get("/foo?bar=baz")));
} }
@@ -88,7 +83,7 @@ public class HttpDocumentationTests {
this.snippet.expectHttpRequest("get-request-with-parameter").withContents( this.snippet.expectHttpRequest("get-request-with-parameter").withContents(
httpRequest(GET, "/foo?b%26r=baz").header(HttpHeaders.HOST, "localhost")); httpRequest(GET, "/foo?b%26r=baz").header(HttpHeaders.HOST, "localhost"));
documentHttpRequest("get-request-with-parameter", null).handle( new HttpRequestSnippet().document("get-request-with-parameter",
result(get("/foo").param("b&r", "baz"))); result(get("/foo").param("b&r", "baz")));
} }
@@ -98,7 +93,7 @@ public class HttpDocumentationTests {
httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content( httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world")); "Hello, world"));
documentHttpRequest("post-request-with-content", null).handle( new HttpRequestSnippet().document("post-request-with-content",
result(post("/foo").content("Hello, world"))); result(post("/foo").content("Hello, world")));
} }
@@ -109,7 +104,7 @@ public class HttpDocumentationTests {
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Type", "application/x-www-form-urlencoded")
.content("b%26r=baz&a=alpha")); .content("b%26r=baz&a=alpha"));
documentHttpRequest("post-request-with-parameter", null).handle( new HttpRequestSnippet().document("post-request-with-parameter",
result(post("/foo").param("b&r", "baz").param("a", "alpha"))); result(post("/foo").param("b&r", "baz").param("a", "alpha")));
} }
@@ -119,7 +114,7 @@ public class HttpDocumentationTests {
httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content( httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world")); "Hello, world"));
documentHttpRequest("put-request-with-content", null).handle( new HttpRequestSnippet().document("put-request-with-content",
result(put("/foo").content("Hello, world"))); result(put("/foo").content("Hello, world")));
} }
@@ -130,48 +125,10 @@ public class HttpDocumentationTests {
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Type", "application/x-www-form-urlencoded")
.content("b%26r=baz&a=alpha")); .content("b%26r=baz&a=alpha"));
documentHttpRequest("put-request-with-parameter", null).handle( new HttpRequestSnippet().document("put-request-with-parameter",
result(put("/foo").param("b&r", "baz").param("a", "alpha"))); result(put("/foo").param("b&r", "baz").param("a", "alpha")));
} }
@Test
public void basicResponse() throws IOException {
this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK));
documentHttpResponse("basic-response", null).handle(result());
}
@Test
public void nonOkResponse() throws IOException {
this.snippet.expectHttpResponse("non-ok-response").withContents(
httpResponse(BAD_REQUEST));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setStatus(BAD_REQUEST.value());
documentHttpResponse("non-ok-response", null).handle(result(response));
}
@Test
public void responseWithHeaders() throws IOException {
this.snippet.expectHttpResponse("response-with-headers").withContents(
httpResponse(OK) //
.header("Content-Type", "application/json") //
.header("a", "alpha"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setHeader("a", "alpha");
documentHttpResponse("response-with-headers", null).handle(result(response));
}
@Test
public void responseWithContent() throws IOException {
this.snippet.expectHttpResponse("response-with-content").withContents(
httpResponse(OK).content("content"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("content");
documentHttpResponse("response-with-content", null).handle(result(response));
}
@Test @Test
public void multipartPost() throws IOException { public void multipartPost() throws IOException {
String expectedContent = createPart(String.format("Content-Disposition: " String expectedContent = createPart(String.format("Content-Disposition: "
@@ -184,7 +141,7 @@ public class HttpDocumentationTests {
.content(expectedContent)); .content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image", MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "<< data >>".getBytes()); "documents/images/example.png", null, "<< data >>".getBytes());
documentHttpRequest("multipart-post", null).handle( new HttpRequestSnippet().document("multipart-post",
result(fileUpload("/upload").file(multipartFile))); result(fileUpload("/upload").file(multipartFile)));
} }
@@ -207,7 +164,8 @@ public class HttpDocumentationTests {
.content(expectedContent)); .content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image", MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "<< data >>".getBytes()); "documents/images/example.png", null, "<< data >>".getBytes());
documentHttpRequest("multipart-post", null).handle( new HttpRequestSnippet().document(
"multipart-post",
result(fileUpload("/upload").file(multipartFile) result(fileUpload("/upload").file(multipartFile)
.param("a", "apple", "avocado").param("b", "banana"))); .param("a", "apple", "avocado").param("b", "banana")));
} }
@@ -226,7 +184,8 @@ public class HttpDocumentationTests {
MockMultipartFile multipartFile = new MockMultipartFile("image", MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", MediaType.IMAGE_PNG_VALUE, "documents/images/example.png", MediaType.IMAGE_PNG_VALUE,
"<< data >>".getBytes()); "<< data >>".getBytes());
documentHttpRequest("multipart-post-with-content-type", null).handle( new HttpRequestSnippet().document(
"multipart-post-with-content-type",
result(fileUpload("/upload").file(multipartFile))); result(fileUpload("/upload").file(multipartFile)));
} }
@@ -238,7 +197,7 @@ public class HttpDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com"); request.setServerName("api.example.com");
documentHttpRequest("get-request-custom-server-name", null).handle( new HttpRequestSnippet().document("get-request-custom-server-name",
result(request)); result(request));
} }
@@ -247,7 +206,7 @@ public class HttpDocumentationTests {
this.snippet.expectHttpRequest("get-request-custom-host").withContents( this.snippet.expectHttpRequest("get-request-custom-host").withContents(
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com"));
documentHttpRequest("get-request-custom-host", null).handle( new HttpRequestSnippet().document("get-request-custom-host",
result(get("/foo").header(HttpHeaders.HOST, "api.example.com"))); result(get("/foo").header(HttpHeaders.HOST, "api.example.com")));
} }
@@ -263,25 +222,8 @@ public class HttpDocumentationTests {
"src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); "src/test/resources/custom-snippet-templates/http-request-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver)); resolver));
documentHttpRequest("request-with-snippet-attributes", new HttpRequestSnippet(attributes(key("title").value(
attributes(key("title").value("Title for the request"))).handle( "Title for the request"))).document("request-with-snippet-attributes",
result(request));
}
@Test
public void responseWithCustomSnippetAttributes() throws IOException {
this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents(
containsString("Title for the response"));
MockHttpServletRequest request = new MockHttpServletRequest();
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("http-response"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/http-response-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentHttpResponse("response-with-snippet-attributes",
attributes(key("title").value("Title for the response"))).handle(
result(request)); result(request));
} }
@@ -290,12 +232,12 @@ public class HttpDocumentationTests {
} }
private String createPart(String content, boolean last) { private String createPart(String content, boolean last) {
String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
StringBuilder part = new StringBuilder(); StringBuilder part = new StringBuilder();
part.append(String.format("--%s%n%s%n", boundary, content)); part.append(String.format("--%s%n%s%n", BOUNDARY, content));
if (last) { if (last) {
part.append(String.format("--%s--", boundary)); part.append(String.format("--%s--", BOUNDARY));
} }
return part.toString(); return part.toString();
} }
} }

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.http;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.httpResponse;
import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
* Tests for {@link HttpResponseSnippet}
*
* @author Andy Wilkinson
* @author Jonathan Pearlin
*/
public class HttpResponseDocumentationHandlerTests {
@Rule
public final ExpectedSnippet snippet = new ExpectedSnippet();
@Test
public void basicResponse() throws IOException {
this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK));
new HttpResponseSnippet().document("basic-response", result());
}
@Test
public void nonOkResponse() throws IOException {
this.snippet.expectHttpResponse("non-ok-response").withContents(
httpResponse(BAD_REQUEST));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setStatus(BAD_REQUEST.value());
new HttpResponseSnippet().document("non-ok-response",
result(response));
}
@Test
public void responseWithHeaders() throws IOException {
this.snippet.expectHttpResponse("response-with-headers").withContents(
httpResponse(OK) //
.header("Content-Type", "application/json") //
.header("a", "alpha"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setHeader("a", "alpha");
new HttpResponseSnippet().document("response-with-headers",
result(response));
}
@Test
public void responseWithContent() throws IOException {
this.snippet.expectHttpResponse("response-with-content").withContents(
httpResponse(OK).content("content"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("content");
new HttpResponseSnippet().document("response-with-content",
result(response));
}
@Test
public void responseWithCustomSnippetAttributes() throws IOException {
this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents(
containsString("Title for the response"));
MockHttpServletRequest request = new MockHttpServletRequest();
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("http-response"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/http-response-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
new HttpResponseSnippet(attributes(key("title").value(
"Title for the response"))).document("response-with-snippet-attributes",
result(request));
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.hypermedia;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
/**
* Tests for {@link ContentTypeLinkExtractor}.
*
* @author Andy Wilkinson
*/
public class ContentTypeLinkExtractorTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private final MockHttpServletResponse response = new MockHttpServletResponse();
@Test
public void extractionFailsWithNullContentType() throws IOException {
this.thrown.expect(IllegalStateException.class);
new ContentTypeLinkExtractor().extractLinks(this.response);
}
@Test
public void extractorCalledWithMatchingContextType() throws IOException {
Map<MediaType, LinkExtractor> extractors = new HashMap<>();
LinkExtractor extractor = mock(LinkExtractor.class);
extractors.put(MediaType.APPLICATION_JSON, extractor);
this.response.setContentType("application/json");
new ContentTypeLinkExtractor(extractors).extractLinks(this.response);
verify(extractor).extractLinks(this.response);
}
@Test
public void extractorCalledWithCompatibleContextType() throws IOException {
Map<MediaType, LinkExtractor> extractors = new HashMap<>();
LinkExtractor extractor = mock(LinkExtractor.class);
extractors.put(MediaType.APPLICATION_JSON, extractor);
this.response.setContentType("application/json;foo=bar");
new ContentTypeLinkExtractor(extractors).extractLinks(this.response);
verify(extractor).extractLinks(this.response);
}
}

View File

@@ -37,7 +37,8 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
/** /**
* Parameterized tests for {@link LinkExtractors} with various payloads. * Parameterized tests for {@link HalLinkExtractor} and {@link AtomLinkExtractor} with
* various payloads.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
@@ -50,8 +51,8 @@ public class LinkExtractorsPayloadTests {
@Parameters @Parameters
public static Collection<Object[]> data() { public static Collection<Object[]> data() {
return Arrays.asList(new Object[] { LinkExtractors.halLinks(), "hal" }, return Arrays.asList(new Object[] { new HalLinkExtractor(), "hal" },
new Object[] { LinkExtractors.atomLinks(), "atom" }); new Object[] { new AtomLinkExtractor(), "atom" });
} }
public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) { public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) {

View File

@@ -1,65 +0,0 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.hypermedia;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import org.junit.Test;
import org.springframework.restdocs.hypermedia.LinkExtractors.AtomLinkExtractor;
import org.springframework.restdocs.hypermedia.LinkExtractors.HalLinkExtractor;
/**
* Tests for {@link LinkExtractors}.
*
* @author Andy Wilkinson
*/
public class LinkExtractorsTests {
@Test
public void nullContentTypeYieldsNullExtractor() {
assertThat(LinkExtractors.extractorForContentType(null), nullValue());
}
@Test
public void emptyContentTypeYieldsNullExtractor() {
assertThat(LinkExtractors.extractorForContentType(""), nullValue());
}
@Test
public void applicationJsonContentTypeYieldsAtomExtractor() {
LinkExtractor linkExtractor = LinkExtractors
.extractorForContentType("application/json");
assertThat(linkExtractor, instanceOf(AtomLinkExtractor.class));
}
@Test
public void applicationHalJsonContentTypeYieldsHalExtractor() {
LinkExtractor linkExtractor = LinkExtractors
.extractorForContentType("application/hal+json");
assertThat(linkExtractor, instanceOf(HalLinkExtractor.class));
}
@Test
public void contentTypeWithParameterYieldsExtractor() {
LinkExtractor linkExtractor = LinkExtractors
.extractorForContentType("application/json;foo=bar");
assertThat(linkExtractor, instanceOf(AtomLinkExtractor.class));
}
}

View File

@@ -20,13 +20,14 @@ import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks;
import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result; import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
@@ -34,7 +35,7 @@ import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
@@ -43,11 +44,11 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
/** /**
* Tests for {@link HypermediaDocumentation} * Tests for {@link LinksSnippet}
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public class HypermediaDocumentationTests { public class LinksDocumentationHandlerTests {
@Rule @Rule
public ExpectedSnippet snippet = new ExpectedSnippet(); public ExpectedSnippet snippet = new ExpectedSnippet();
@@ -57,21 +58,22 @@ public class HypermediaDocumentationTests {
@Test @Test
public void undocumentedLink() throws IOException { public void undocumentedLink() throws IOException {
this.thrown.expect(SnippetGenerationException.class); this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not" this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " documented: [foo]")); + " documented: [foo]"));
documentLinks("undocumented-link", null, new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo",
new StubLinkExtractor().withLinks(new Link("foo", "bar"))).handle( "bar")), Collections.<LinkDescriptor> emptyList()).document(
result()); "undocumented-link", result());
} }
@Test @Test
public void missingLink() throws IOException { public void missingLink() throws IOException {
this.thrown.expect(SnippetGenerationException.class); this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not" this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " found in the response: [foo]")); + " found in the response: [foo]"));
documentLinks("missing-link", null, new StubLinkExtractor(), new LinksSnippet(new StubLinkExtractor(),
new LinkDescriptor("foo").description("bar")).handle(result()); Arrays.asList(new LinkDescriptor("foo").description("bar"))).document(
"missing-link", result());
} }
@Test @Test
@@ -79,9 +81,9 @@ public class HypermediaDocumentationTests {
this.snippet.expectLinks("documented-optional-link").withContents( // this.snippet.expectLinks("documented-optional-link").withContents( //
tableWithHeader("Relation", "Description") // tableWithHeader("Relation", "Description") //
.row("foo", "bar")); .row("foo", "bar"));
documentLinks("documented-optional-link", null, new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo",
new StubLinkExtractor().withLinks(new Link("foo", "blah")), "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar")
new LinkDescriptor("foo").description("bar").optional()).handle(result()); .optional())).document("documented-optional-link", result());
} }
@Test @Test
@@ -89,19 +91,20 @@ public class HypermediaDocumentationTests {
this.snippet.expectLinks("missing-optional-link").withContents( // this.snippet.expectLinks("missing-optional-link").withContents( //
tableWithHeader("Relation", "Description") // tableWithHeader("Relation", "Description") //
.row("foo", "bar")); .row("foo", "bar"));
documentLinks("missing-optional-link", null, new StubLinkExtractor(), new LinksSnippet(new StubLinkExtractor(),
new LinkDescriptor("foo").description("bar").optional()).handle(result()); Arrays.asList(new LinkDescriptor("foo").description("bar").optional()))
.document("missing-optional-link", result());
} }
@Test @Test
public void undocumentedLinkAndMissingLink() throws IOException { public void undocumentedLinkAndMissingLink() throws IOException {
this.thrown.expect(SnippetGenerationException.class); this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not" this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " documented: [a]. Links with the following relations were not" + " documented: [a]. Links with the following relations were not"
+ " found in the response: [foo]")); + " found in the response: [foo]"));
documentLinks("undocumented-link-and-missing-link", null, new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
new StubLinkExtractor().withLinks(new Link("a", "alpha")), "alpha")), Arrays.asList(new LinkDescriptor("foo").description("bar")))
new LinkDescriptor("foo").description("bar")).handle(result()); .document("undocumented-link-and-missing-link", result());
} }
@Test @Test
@@ -110,12 +113,11 @@ public class HypermediaDocumentationTests {
tableWithHeader("Relation", "Description") // tableWithHeader("Relation", "Description") //
.row("a", "one") // .row("a", "one") //
.row("b", "two")); .row("b", "two"));
documentLinks( new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"documented-links", "alpha"), new Link("b", "bravo")), Arrays.asList(
null, new LinkDescriptor("a").description("one"),
new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", new LinkDescriptor("b").description("two"))).document("documented-links",
"bravo")), new LinkDescriptor("a").description("one"), result());
new LinkDescriptor("b").description("two")).handle(result());
} }
@Test @Test
@@ -132,20 +134,18 @@ public class HypermediaDocumentationTests {
"src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver)); resolver));
documentLinks( new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"links-with-custom-descriptor-attributes", "alpha"), new Link("b", "bravo")), Arrays.asList(
null,
new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b",
"bravo")),
new LinkDescriptor("a").description("one").attributes( new LinkDescriptor("a").description("one").attributes(
key("foo").value("alpha")), key("foo").value("alpha")),
new LinkDescriptor("b").description("two").attributes( new LinkDescriptor("b").description("two").attributes(
key("foo").value("bravo"))).handle(result(request)); key("foo").value("bravo")))).document(
"links-with-custom-descriptor-attributes", result(request));
} }
@Test @Test
public void linksWithCustomAttribute() throws IOException { public void linksWithCustomAttributes() throws IOException {
this.snippet.expectLinks("links-with-custom-attribute").withContents( this.snippet.expectLinks("links-with-custom-attributes").withContents(
startsWith(".Title for the links")); startsWith(".Title for the links"));
MockHttpServletRequest request = new MockHttpServletRequest(); MockHttpServletRequest request = new MockHttpServletRequest();
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
@@ -155,12 +155,12 @@ public class HypermediaDocumentationTests {
"src/test/resources/custom-snippet-templates/links-with-title.snippet")); "src/test/resources/custom-snippet-templates/links-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver)); resolver));
documentLinks( new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"links-with-custom-attribute", "alpha"), new Link("b", "bravo")), attributes(key("title").value(
attributes(key("title").value("Title for the links")), "Title for the links")), Arrays.asList(
new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", new LinkDescriptor("a").description("one"),
"bravo")), new LinkDescriptor("a").description("one"), new LinkDescriptor("b").description("two"))).document(
new LinkDescriptor("b").description("two")).handle(result(request)); "links-with-custom-attributes", result(request));
} }
private static class StubLinkExtractor implements LinkExtractor { private static class StubLinkExtractor implements LinkExtractor {

View File

@@ -24,7 +24,7 @@ import java.util.Arrays;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetException;
/** /**
* Tests for {@link FieldValidator} * Tests for {@link FieldValidator}
@@ -73,7 +73,7 @@ public class FieldValidatorTests {
@Test @Test
public void missingField() throws IOException { public void missingField() throws IOException {
this.thrownException.expect(SnippetGenerationException.class); this.thrownException.expect(SnippetException.class);
this.thrownException this.thrownException
.expectMessage(equalTo("Fields with the following paths were not found" .expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [y, z]")); + " in the payload: [y, z]"));
@@ -84,7 +84,7 @@ public class FieldValidatorTests {
@Test @Test
public void undocumentedField() throws IOException { public void undocumentedField() throws IOException {
this.thrownException.expect(SnippetGenerationException.class); this.thrownException.expect(SnippetException.class);
this.thrownException.expectMessage(equalTo(String this.thrownException.expectMessage(equalTo(String
.format("The following parts of the payload were not" .format("The following parts of the payload were not"
+ " documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}"))); + " documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}")));

View File

@@ -1,282 +0,0 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.payload;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.snippet.SnippetGenerationException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
* Tests for {@link PayloadDocumentation}
*
* @author Andy Wilkinson
*/
public class PayloadDocumentationTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Rule
public final ExpectedSnippet snippet = new ExpectedSnippet();
@Test
public void mapRequestWithFields() throws IOException {
this.snippet.expectRequestFields("map-request-with-fields").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("a.b", "Number", "one") //
.row("a.c", "String", "two") //
.row("a", "Object", "three"));
documentRequestFields("map-request-with-fields", null,
fieldWithPath("a.b").description("one"),
fieldWithPath("a.c").description("two"),
fieldWithPath("a").description("three")).handle(
result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}")));
}
@Test
public void arrayRequestWithFields() throws IOException {
this.snippet.expectRequestFields("array-request-with-fields").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("[]a.b", "Number", "one") //
.row("[]a.c", "String", "two") //
.row("[]a", "Object", "three"));
documentRequestFields("array-request-with-fields", null,
fieldWithPath("[]a.b").description("one"),
fieldWithPath("[]a.c").description("two"),
fieldWithPath("[]a").description("three")).handle(
result(get("/foo").content(
"[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]")));
}
@Test
public void mapResponseWithFields() throws IOException {
this.snippet.expectResponseFields("map-response-with-fields").withContents(//
tableWithHeader("Path", "Type", "Description") //
.row("id", "Number", "one") //
.row("date", "String", "two") //
.row("assets", "Array", "three") //
.row("assets[]", "Object", "four") //
.row("assets[].id", "Number", "five") //
.row("assets[].name", "String", "six"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append(
"{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":"
+ " [{\"id\":356,\"name\": \"sample\"}]}");
documentResponseFields("map-response-with-fields", null,
fieldWithPath("id").description("one"),
fieldWithPath("date").description("two"),
fieldWithPath("assets").description("three"),
fieldWithPath("assets[]").description("four"),
fieldWithPath("assets[].id").description("five"),
fieldWithPath("assets[].name").description("six")).handle(
result(response));
}
@Test
public void arrayResponseWithFields() throws IOException {
this.snippet.expectResponseFields("array-response-with-fields").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("[]a.b", "Number", "one") //
.row("[]a.c", "String", "two") //
.row("[]a", "Object", "three"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter()
.append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]");
documentResponseFields("array-response-with-fields", null,
fieldWithPath("[]a.b").description("one"),
fieldWithPath("[]a.c").description("two"),
fieldWithPath("[]a").description("three")).handle(result(response));
}
@Test
public void arrayResponse() throws IOException {
this.snippet.expectResponseFields("array-response").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("[]", "String", "one"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("[\"a\", \"b\", \"c\"]");
documentResponseFields("array-response", null,
fieldWithPath("[]").description("one")).handle(result(response));
}
@Test
public void undocumentedRequestField() throws IOException {
this.thrown.expect(SnippetGenerationException.class);
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
documentRequestFields("undocumented-request-fields", null).handle(
result(get("/foo").content("{\"a\": 5}")));
}
@Test
public void missingRequestField() throws IOException {
this.thrown.expect(SnippetGenerationException.class);
this.thrown
.expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
documentRequestFields("missing-request-fields", null,
fieldWithPath("a.b").description("one")).handle(
result(get("/foo").content("{}")));
}
@Test
public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException {
this.thrown.expect(FieldTypeRequiredException.class);
documentRequestFields("missing-optional-request-field-with-no-type", null,
fieldWithPath("a.b").description("one").optional()).handle(
result(get("/foo").content("{ }")));
}
@Test
public void undocumentedRequestFieldAndMissingRequestField() throws IOException {
this.thrown.expect(SnippetGenerationException.class);
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
this.thrown
.expectMessage(endsWith("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
documentRequestFields("undocumented-request-field-and-missing-request-field",
null, fieldWithPath("a.b").description("one")).handle(
result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
}
@Test
public void requestFieldsWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectRequestFields(
"request-fields-with-custom-descriptor-attributes").withContents( //
tableWithHeader("Path", "Type", "Description", "Foo") //
.row("a.b", "Number", "one", "alpha") //
.row("a.c", "String", "two", "bravo") //
.row("a", "Object", "three", "charlie"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-fields"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes());
documentRequestFields(
"request-fields-with-custom-descriptor-attributes",
null,
fieldWithPath("a.b").description("one").attributes(
key("foo").value("alpha")),
fieldWithPath("a.c").description("two").attributes(
key("foo").value("bravo")),
fieldWithPath("a").description("three").attributes(
key("foo").value("charlie"))).handle(result(request));
}
@Test
public void responseFieldsWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectResponseFields("response-fields-with-custom-attributes")
.withContents( //
tableWithHeader("Path", "Type", "Description", "Foo") //
.row("a.b", "Number", "one", "alpha") //
.row("a.c", "String", "two", "bravo") //
.row("a", "Object", "three", "charlie"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("response-fields"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getOutputStream().print("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}");
documentResponseFields(
"response-fields-with-custom-attributes",
null,
fieldWithPath("a.b").description("one").attributes(
key("foo").value("alpha")),
fieldWithPath("a.c").description("two").attributes(
key("foo").value("bravo")),
fieldWithPath("a").description("three").attributes(
key("foo").value("charlie"))).handle(result(request, response));
}
@Test
public void requestFieldsWithCustomAttributes() throws IOException {
this.snippet.expectRequestFields("request-fields-with-custom-attributes")
.withContents(startsWith(".Custom title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-fields"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/request-fields-with-title.snippet"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.setContent("{\"a\": \"foo\"}".getBytes());
MockHttpServletResponse response = new MockHttpServletResponse();
documentRequestFields("request-fields-with-custom-attributes",
attributes(key("title").value("Custom title")),
fieldWithPath("a").description("one")).handle(result(request, response));
}
@Test
public void responseFieldsWithCustomAttributes() throws IOException {
this.snippet.expectResponseFields("response-fields-with-custom-attributes")
.withContents(startsWith(".Custom title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("response-fields"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/response-fields-with-title.snippet"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getOutputStream().print("{\"a\": \"foo\"}");
documentResponseFields("response-fields-with-custom-attributes",
attributes(key("title").value("Custom title")),
fieldWithPath("a").description("one")).handle(result(request, response));
}
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.payload;
import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
* Tests for {@link RequestFieldsSnippet}
*
* @author Andy Wilkinson
*/
public class RequestFieldsDocumentationHandlerTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Rule
public final ExpectedSnippet snippet = new ExpectedSnippet();
@Test
public void mapRequestWithFields() throws IOException {
this.snippet.expectRequestFields("map-request-with-fields").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("a.b", "Number", "one") //
.row("a.c", "String", "two") //
.row("a", "Object", "three"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
.description("one"), fieldWithPath("a.c").description("two"),
fieldWithPath("a").description("three"))).document(
"map-request-with-fields",
result(get("/foo").content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}")));
}
@Test
public void arrayRequestWithFields() throws IOException {
this.snippet.expectRequestFields("array-request-with-fields").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("[]a.b", "Number", "one") //
.row("[]a.c", "String", "two") //
.row("[]a", "Object", "three"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b")
.description("one"), fieldWithPath("[]a.c").description("two"),
fieldWithPath("[]a").description("three"))).document(
"array-request-with-fields",
result(get("/foo").content(
"[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]")));
}
@Test
public void undocumentedRequestField() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
new RequestFieldsSnippet(Collections.<FieldDescriptor> emptyList())
.document("undocumented-request-field",
result(get("/foo").content("{\"a\": 5}")));
}
@Test
public void missingRequestField() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
.description("one"))).document("missing-request-fields",
result(get("/foo").content("{}")));
}
@Test
public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException {
this.thrown.expect(FieldTypeRequiredException.class);
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
.description("one").optional())).document(
"missing-optional-request-field-with-no-type", result(get("/foo")
.content("{ }")));
}
@Test
public void undocumentedRequestFieldAndMissingRequestField() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
this.thrown
.expectMessage(endsWith("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b")
.description("one"))).document(
"undocumented-request-field-and-missing-request-field",
result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
}
@Test
public void requestFieldsWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectRequestFields(
"request-fields-with-custom-descriptor-attributes").withContents( //
tableWithHeader("Path", "Type", "Description", "Foo") //
.row("a.b", "Number", "one", "alpha") //
.row("a.c", "String", "two", "bravo") //
.row("a", "Object", "three", "charlie"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-fields")).thenReturn(
snippetResource("request-fields-with-extra-column"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes());
new RequestFieldsSnippet(Arrays.asList(
fieldWithPath("a.b").description("one").attributes(
key("foo").value("alpha")),
fieldWithPath("a.c").description("two").attributes(
key("foo").value("bravo")),
fieldWithPath("a").description("three").attributes(
key("foo").value("charlie")))).document(
"request-fields-with-custom-descriptor-attributes", result(request));
}
@Test
public void requestFieldsWithCustomAttributes() throws IOException {
this.snippet.expectRequestFields("request-fields-with-custom-attributes")
.withContents(startsWith(".Custom title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-fields")).thenReturn(
snippetResource("request-fields-with-title"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.setContent("{\"a\": \"foo\"}".getBytes());
MockHttpServletResponse response = new MockHttpServletResponse();
new RequestFieldsSnippet(attributes(key("title").value(
"Custom title")), Arrays.asList(fieldWithPath("a").description("one")))
.document("request-fields-with-custom-attributes",
result(request, response));
}
private FileSystemResource snippetResource(String name) {
return new FileSystemResource("src/test/resources/custom-snippet-templates/"
+ name + ".snippet");
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.payload;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import java.io.IOException;
import java.util.Arrays;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
* Tests for {@link PayloadDocumentation}
*
* @author Andy Wilkinson
*/
public class ResponseFieldsDocumentationHandlerTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Rule
public final ExpectedSnippet snippet = new ExpectedSnippet();
@Test
public void mapResponseWithFields() throws IOException {
this.snippet.expectResponseFields("map-response-with-fields").withContents(//
tableWithHeader("Path", "Type", "Description") //
.row("id", "Number", "one") //
.row("date", "String", "two") //
.row("assets", "Array", "three") //
.row("assets[]", "Object", "four") //
.row("assets[].id", "Number", "five") //
.row("assets[].name", "String", "six"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append(
"{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":"
+ " [{\"id\":356,\"name\": \"sample\"}]}");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id")
.description("one"), fieldWithPath("date").description("two"),
fieldWithPath("assets").description("three"), fieldWithPath("assets[]")
.description("four"),
fieldWithPath("assets[].id").description("five"),
fieldWithPath("assets[].name").description("six"))).document(
"map-response-with-fields", result(response));
}
@Test
public void arrayResponseWithFields() throws IOException {
this.snippet.expectResponseFields("array-response-with-fields").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("[]a.b", "Number", "one") //
.row("[]a.c", "String", "two") //
.row("[]a", "Object", "three"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter()
.append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b")
.description("one"), fieldWithPath("[]a.c").description("two"),
fieldWithPath("[]a").description("three"))).document(
"array-response-with-fields", result(response));
}
@Test
public void arrayResponse() throws IOException {
this.snippet.expectResponseFields("array-response").withContents( //
tableWithHeader("Path", "Type", "Description") //
.row("[]", "String", "one"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("[\"a\", \"b\", \"c\"]");
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]")
.description("one"))).document("array-response", result(response));
}
@Test
public void responseFieldsWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectResponseFields("response-fields-with-custom-attributes")
.withContents( //
tableWithHeader("Path", "Type", "Description", "Foo") //
.row("a.b", "Number", "one", "alpha") //
.row("a.c", "String", "two", "bravo") //
.row("a", "Object", "three", "charlie"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("response-fields")).thenReturn(
snippetResource("response-fields-with-extra-column"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getOutputStream().print("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}");
new ResponseFieldsSnippet(Arrays.asList(
fieldWithPath("a.b").description("one").attributes(
key("foo").value("alpha")),
fieldWithPath("a.c").description("two").attributes(
key("foo").value("bravo")),
fieldWithPath("a").description("three").attributes(
key("foo").value("charlie")))).document(
"response-fields-with-custom-attributes", result(request, response));
}
@Test
public void responseFieldsWithCustomAttributes() throws IOException {
this.snippet.expectResponseFields("response-fields-with-custom-attributes")
.withContents(startsWith(".Custom title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("response-fields")).thenReturn(
snippetResource("response-fields-with-title"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getOutputStream().print("{\"a\": \"foo\"}");
new ResponseFieldsSnippet(attributes(key("title").value(
"Custom title")), Arrays.asList(fieldWithPath("a").description("one")))
.document("response-fields-with-custom-attributes",
result(request, response));
}
private FileSystemResource snippetResource(String name) {
return new FileSystemResource("src/test/resources/custom-snippet-templates/"
+ name + ".snippet");
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.request;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import static org.springframework.restdocs.test.TestRequestBuilders.get;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.restdocs.config.RestDocumentationContext;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
* Tests for {@link PathParametersSnippet}
*
* @author awilkinson
*
*/
public class PathParametersDocumentationHandlerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public ExpectedSnippet snippet = new ExpectedSnippet();
@Test
public void undocumentedPathParameter() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Path parameters with the following names were"
+ " not documented: [a]"));
new PathParametersSnippet(Collections.<ParameterDescriptor> emptyList())
.document("undocumented-path-parameter", result(get("/{a}/", "alpha")));
}
@Test
public void missingPathParameter() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Path parameters with the following names were"
+ " not found in the request: [a]"));
new PathParametersSnippet(
Arrays.asList(parameterWithName("a").description("one"))).document(
"missing-path-parameter", result(get("/")));
}
@Test
public void undocumentedAndMissingPathParameters() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Path parameters with the following names were"
+ " not documented: [b]. Path parameters with the following"
+ " names were not found in the request: [a]"));
new PathParametersSnippet(
Arrays.asList(parameterWithName("a").description("one"))).document(
"undocumented-and-missing-path-parameters", result(get("/{b}", "bravo")));
}
@Test
public void pathParameters() throws IOException {
this.snippet.expectPathParameters("path-parameters").withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row("b",
"two"));
new PathParametersSnippet(Arrays.asList(
parameterWithName("a").description("one"), parameterWithName("b")
.description("two"))).document(
"path-parameters",
result(get("/{a}/{b}", "alpha", "banana").requestAttr(
RestDocumentationContext.class.getName(),
new RestDocumentationContext(null))));
}
@Test
public void pathParametersWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectPathParameters(
"path-parameters-with-custom-descriptor-attributes").withContents(
tableWithHeader("Parameter", "Description", "Foo").row("a", "one",
"alpha").row("b", "two", "bravo"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("path-parameters")).thenReturn(
snippetResource("path-parameters-with-extra-column"));
new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one")
.attributes(key("foo").value("alpha")), parameterWithName("b")
.description("two").attributes(key("foo").value("bravo")))).document(
"path-parameters-with-custom-descriptor-attributes",
result(get("{a}/{b}", "alpha", "bravo").requestAttr(
TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))));
}
@Test
public void pathParametersWithCustomAttributes() throws IOException {
this.snippet.expectPathParameters("path-parameters-with-custom-attributes")
.withContents(startsWith(".The title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("path-parameters")).thenReturn(
snippetResource("path-parameters-with-title"));
new PathParametersSnippet(
attributes(key("title").value("The title")),
Arrays.asList(
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")), parameterWithName("b")
.description("two").attributes(key("foo").value("bravo"))))
.document(
"path-parameters-with-custom-attributes",
result(get("{a}/{b}", "alpha", "bravo").requestAttr(
TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))));
}
private FileSystemResource snippetResource(String name) {
return new FileSystemResource("src/test/resources/custom-snippet-templates/"
+ name + ".snippet");
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.request;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import static org.springframework.restdocs.test.TestRequestBuilders.get;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.restdocs.config.RestDocumentationContext;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
* Tests for {@link QueryParametersSnippet}
*
* @author Andy Wilkinson
*/
public class QueryParametersDocumentationHandlerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public ExpectedSnippet snippet = new ExpectedSnippet();
@Test
public void undocumentedQueryParameter() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(equalTo("Query parameters with the following names were"
+ " not documented: [a]"));
new QueryParametersSnippet(Collections.<ParameterDescriptor> emptyList())
.document("undocumented-query-parameter",
result(get("/").param("a", "alpha")));
}
@Test
public void missingQueryParameter() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(equalTo("Query parameters with the following names were"
+ " not found in the request: [a]"));
new QueryParametersSnippet(Arrays.asList(parameterWithName("a")
.description("one"))).document("missing-query-parameter",
result(get("/")));
}
@Test
public void undocumentedAndMissingQueryParameters() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown
.expectMessage(equalTo("Query parameters with the following names were"
+ " not documented: [b]. Query parameters with the following"
+ " names were not found in the request: [a]"));
new QueryParametersSnippet(Arrays.asList(parameterWithName("a")
.description("one"))).document(
"undocumented-and-missing-query-parameters",
result(get("/").param("b", "bravo")));
}
@Test
public void queryParameterSnippetFromRequestParameters() throws IOException {
this.snippet.expectQueryParameters("query-parameter-snippet-request-parameters")
.withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row(
"b", "two"));
new QueryParametersSnippet(Arrays.asList(parameterWithName("a")
.description("one"), parameterWithName("b").description("two")))
.document("query-parameter-snippet-request-parameters", result(get("/")
.param("a", "bravo").param("b", "bravo")));
}
@Test
public void queryParameterSnippetFromRequestUriQueryString() throws IOException {
this.snippet.expectQueryParameters(
"query-parameter-snippet-request-uri-query-string").withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row("b",
"two"));
new QueryParametersSnippet(Arrays.asList(parameterWithName("a")
.description("one"), parameterWithName("b").description("two")))
.document(
"query-parameter-snippet-request-uri-query-string",
result(get("/?a=alpha&b=bravo").requestAttr(
RestDocumentationContext.class.getName(),
new RestDocumentationContext(null))));
}
@Test
public void queryParametersWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectQueryParameters(
"query-parameters-with-custom-descriptor-attributes").withContents(
tableWithHeader("Parameter", "Description", "Foo").row("a", "one",
"alpha").row("b", "two", "bravo"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("query-parameters")).thenReturn(
snippetResource("query-parameters-with-extra-column"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.addParameter("a", "bravo");
request.addParameter("b", "bravo");
new QueryParametersSnippet(Arrays.asList(
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")),
parameterWithName("b").description("two").attributes(
key("foo").value("bravo")))).document(
"query-parameters-with-custom-descriptor-attributes", result(request));
}
@Test
public void queryParametersWithCustomAttributes() throws IOException {
this.snippet.expectQueryParameters("query-parameters-with-custom-attributes")
.withContents(startsWith(".The title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("query-parameters")).thenReturn(
snippetResource("query-parameters-with-title"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.addParameter("a", "bravo");
request.addParameter("b", "bravo");
new QueryParametersSnippet(
attributes(key("title").value("The title")),
Arrays.asList(
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")), parameterWithName("b")
.description("two").attributes(key("foo").value("bravo"))))
.document("query-parameters-with-custom-attributes", result(request));
}
private FileSystemResource snippetResource(String name) {
return new FileSystemResource("src/test/resources/custom-snippet-templates/"
+ name + ".snippet");
}
}

View File

@@ -1,252 +0,0 @@
/*
* Copyright 2014-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.restdocs.request;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.request.RequestDocumentation.documentPathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
import static org.springframework.restdocs.test.TestRequestBuilders.get;
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.restdocs.config.RestDocumentationContext;
import org.springframework.restdocs.snippet.SnippetGenerationException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
* Requests for {@link RequestDocumentation}
*
* @author Andy Wilkinson
*/
public class RequestDocumentationTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Rule
public ExpectedSnippet snippet = new ExpectedSnippet();
@Test
public void undocumentedQueryParameter() throws IOException {
this.thrown.expect(SnippetGenerationException.class);
this.thrown
.expectMessage(equalTo("Query parameters with the following names were"
+ " not documented: [a]"));
documentQueryParameters("undocumented-query-parameter", null).handle(
result(get("/").param("a", "alpha")));
}
@Test
public void missingQueryParameter() throws IOException {
this.thrown.expect(SnippetGenerationException.class);
this.thrown
.expectMessage(equalTo("Query parameters with the following names were"
+ " not found in the request: [a]"));
documentQueryParameters("missing-query-parameter", null,
parameterWithName("a").description("one")).handle(result(get("/")));
}
@Test
public void undocumentedAndMissingQueryParameters() throws IOException {
this.thrown.expect(SnippetGenerationException.class);
this.thrown
.expectMessage(equalTo("Query parameters with the following names were"
+ " not documented: [b]. Query parameters with the following"
+ " names were not found in the request: [a]"));
documentQueryParameters("undocumented-and-missing-query-parameters", null,
parameterWithName("a").description("one")).handle(
result(get("/").param("b", "bravo")));
}
@Test
public void queryParameterSnippetFromRequestParameters() throws IOException {
this.snippet.expectQueryParameters("query-parameter-snippet-request-parameters")
.withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row(
"b", "two"));
documentQueryParameters("query-parameter-snippet-request-parameters", null,
parameterWithName("a").description("one"),
parameterWithName("b").description("two")).handle(
result(get("/").param("a", "bravo").param("b", "bravo")));
}
@Test
public void queryParameterSnippetFromRequestUriQueryString() throws IOException {
this.snippet.expectQueryParameters(
"query-parameter-snippet-request-uri-query-string").withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row("b",
"two"));
documentQueryParameters("query-parameter-snippet-request-uri-query-string", null,
parameterWithName("a").description("one"),
parameterWithName("b").description("two")).handle(
result(get("/?a=alpha&b=bravo").requestAttr(
RestDocumentationContext.class.getName(),
new RestDocumentationContext())));
}
@Test
public void queryParametersWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectQueryParameters(
"query-parameters-with-custom-descriptor-attributes").withContents(
tableWithHeader("Parameter", "Description", "Foo").row("a", "one",
"alpha").row("b", "two", "bravo"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("query-parameters"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.addParameter("a", "bravo");
request.addParameter("b", "bravo");
documentQueryParameters(
"query-parameters-with-custom-descriptor-attributes",
null,
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")),
parameterWithName("b").description("two").attributes(
key("foo").value("bravo"))).handle(result(request));
}
@Test
public void queryParametersWithCustomAttributes() throws IOException {
this.snippet.expectQueryParameters("query-parameters-with-custom-attributes")
.withContents(startsWith(".The title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("query-parameters"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.addParameter("a", "bravo");
request.addParameter("b", "bravo");
documentQueryParameters(
"query-parameters-with-custom-attributes",
attributes(key("title").value("The title")),
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")),
parameterWithName("b").description("two").attributes(
key("foo").value("bravo"))).handle(result(request));
}
@Test
public void undocumentedPathParameter() throws IOException {
this.thrown.expect(SnippetGenerationException.class);
this.thrown.expectMessage(equalTo("Path parameters with the following names were"
+ " not documented: [a]"));
documentPathParameters("undocumented-path-parameter", null).handle(
result(get("/{a}/", "alpha")));
}
@Test
public void missingPathParameter() throws IOException {
this.thrown.expect(SnippetGenerationException.class);
this.thrown.expectMessage(equalTo("Path parameters with the following names were"
+ " not found in the request: [a]"));
documentPathParameters("missing-path-parameter", null,
parameterWithName("a").description("one")).handle(result(get("/")));
}
@Test
public void undocumentedAndMissingPathParameters() throws IOException {
this.thrown.expect(SnippetGenerationException.class);
this.thrown.expectMessage(equalTo("Path parameters with the following names were"
+ " not documented: [b]. Path parameters with the following"
+ " names were not found in the request: [a]"));
documentPathParameters("undocumented-and-missing-path-parameters", null,
parameterWithName("a").description("one")).handle(
result(get("/{b}", "bravo")));
}
@Test
public void pathParameters() throws IOException {
this.snippet.expectPathParameters("path-parameters").withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row("b",
"two"));
documentPathParameters("path-parameters", null,
parameterWithName("a").description("one"),
parameterWithName("b").description("two")).handle(
result(get("/{a}/{b}", "alpha", "banana").requestAttr(
RestDocumentationContext.class.getName(),
new RestDocumentationContext())));
}
@Test
public void pathParametersWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectPathParameters(
"path-parameters-with-custom-descriptor-attributes").withContents(
tableWithHeader("Parameter", "Description", "Foo").row("a", "one",
"alpha").row("b", "two", "bravo"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("path-parameters"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet"));
documentPathParameters(
"path-parameters-with-custom-descriptor-attributes",
null,
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")),
parameterWithName("b").description("two").attributes(
key("foo").value("bravo"))).handle(
result(get("{a}/{b}", "alpha", "bravo").requestAttr(
TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))));
}
@Test
public void pathParametersWithCustomAttributes() throws IOException {
this.snippet.expectPathParameters("path-parameters-with-custom-attributes")
.withContents(startsWith(".The title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("path-parameters"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet"));
documentPathParameters(
"path-parameters-with-custom-attributes",
attributes(key("title").value("The title")),
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")),
parameterWithName("b").description("two").attributes(
key("foo").value("bravo"))).handle(
result(get("{a}/{b}", "alpha", "bravo").requestAttr(
TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))));
}
}

View File

@@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock;
import java.io.File; import java.io.File;
import org.junit.Test; import org.junit.Test;
import org.springframework.restdocs.snippet.StandardWriterResolver;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
/** /**

View File

@@ -26,12 +26,12 @@ import org.hamcrest.Matcher;
import org.junit.rules.TestRule; import org.junit.rules.TestRule;
import org.junit.runner.Description; import org.junit.runner.Description;
import org.junit.runners.model.Statement; import org.junit.runners.model.Statement;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler; import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher;
/** /**
* The {@code ExpectedSnippet} rule is used to verify that a * The {@code ExpectedSnippet} rule is used to verify that a
* {@link SnippetWritingResultHandler} has generated the expected snippet. * {@link TemplatedSnippet} has generated the expected snippet.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */

View File

@@ -92,7 +92,7 @@ public class StubMvcResult implements MvcResult {
this.request.setAttribute(TemplateEngine.class.getName(), this.request.setAttribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(new StandardTemplateResourceResolver())); new MustacheTemplateEngine(new StandardTemplateResourceResolver()));
} }
RestDocumentationContext context = new RestDocumentationContext(); RestDocumentationContext context = new RestDocumentationContext(null);
this.request.setAttribute(RestDocumentationContext.class.getName(), context); this.request.setAttribute(RestDocumentationContext.class.getName(), context);
if (this.request.getAttribute(WriterResolver.class.getName()) == null) { if (this.request.getAttribute(WriterResolver.class.getName()) == null) {
this.request.setAttribute(WriterResolver.class.getName(), this.request.setAttribute(WriterResolver.class.getName(),

View File

@@ -29,7 +29,8 @@ public class TestRequestBuilders {
Object... urlVariables) { Object... urlVariables) {
return org.springframework.restdocs.RestDocumentationRequestBuilders.get( return org.springframework.restdocs.RestDocumentationRequestBuilders.get(
urlTemplate, urlVariables).requestAttr( urlTemplate, urlVariables).requestAttr(
RestDocumentationContext.class.getName(), new RestDocumentationContext()); RestDocumentationContext.class.getName(),
new RestDocumentationContext(null));
} }
} }