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]]
=== 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]
----
<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
`org.springframework.restdocs.hypermedia.HypermediaDocumentation`.
<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`.
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]
----
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
`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
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]]
=== 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]
----
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
`org.springframework.restdocs.request.RequestDocumentation`.
<3> Document a parameter named `per_page`.
@@ -199,14 +201,15 @@ is not found in the request.
[[documenting-your-api-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]
----
include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters]
----
<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
`org.springframework.restdocs.request.RequestDocumentation`.
<4> Document a parameter named `latitude`.
@@ -244,6 +247,9 @@ documented
| 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]]

View File

@@ -200,7 +200,7 @@ be included in the project's jar:
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<configuration>
<outputDirectory>
${project.build.outputDirectory}/static/docs
</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
`org.springframework.restdocs.RestDocumentation`. `RestDocumentationConfigurer` applies
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.test.web.servlet.result.MockMvcResultMatchers.status;
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.test.web.servlet.MockMvc;
@@ -29,13 +30,13 @@ public class Hypermedia {
private MockMvc mockMvc;
public void links() throws Exception {
public void defaultExtractor() throws Exception {
// tag::links[]
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("index").withLinks( // <1>
.andDo(document("index", links( // <1>
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[]
}
@@ -43,9 +44,9 @@ public class Hypermedia {
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
//tag::explicit-extractor[]
.andDo(document("index").withLinks(halLinks(), // <1>
.andDo(document("index", links(halLinks(), // <1>
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[]
}

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ package com.example;
import static org.springframework.restdocs.RestDocumentation.document;
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.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -27,14 +28,14 @@ public class QueryParameters {
private MockMvc mockMvc;
public void queryParameters() throws Exception {
public void queryParametersSnippet() throws Exception {
// tag::query-parameters[]
this.mockMvc.perform(get("/users?page=2&per_page=100"))
.andExpect(status().isOk())
.andDo(document("users").withQueryParameters( // <1>
.andDo(document("users", queryParameters( // <1>
parameterWithName("page").description("The page to retrieve"), // <2>
parameterWithName("per_page").description("Entries per page") // <3>
));
)));
// 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.document;
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.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
@@ -100,26 +103,26 @@ public class ApiDocumentation {
.andExpect(jsonPath("timestamp", is(notNullValue())))
.andExpect(jsonPath("status", is(400)))
.andExpect(jsonPath("path", is(notNullValue())))
.andDo(document("error-example")
.withResponseFields(
.andDo(document("error-example",
responseFields(
fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"),
fieldWithPath("message").description("A description of the cause of the error"),
fieldWithPath("path").description("The path to which the request was made"),
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
public void indexExample() throws Exception {
this.mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andDo(document("index-example")
.withLinks(
.andDo(document("index-example",
links(
linkWithRel("notes").description("The <<resources-notes,Notes resource>>"),
linkWithRel("tags").description("The <<resources-tags,Tags resource>>"),
linkWithRel("profile").description("The ALPS profile for the service"))
.withResponseFields(
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources")));
linkWithRel("profile").description("The ALPS profile for the service")),
responseFields(
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources"))));
}
@@ -135,9 +138,9 @@ public class ApiDocumentation {
this.mockMvc.perform(get("/notes"))
.andExpect(status().isOk())
.andDo(document("notes-list-example")
.withResponseFields(
fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>")));
.andDo(document("notes-list-example",
responseFields(
fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"))));
}
@Test
@@ -161,11 +164,11 @@ public class ApiDocumentation {
post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note))).andExpect(
status().isCreated())
.andDo(document("notes-create-example")
.withRequestFields(
.andDo(document("notes-create-example",
requestFields(
fieldWithPath("title").description("The title 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
@@ -198,14 +201,14 @@ public class ApiDocumentation {
.andExpect(jsonPath("body", is(note.get("body"))))
.andExpect(jsonPath("_links.self.href", is(noteLocation)))
.andExpect(jsonPath("_links.tags", is(notNullValue())))
.andDo(document("note-get-example")
.withLinks(
.andDo(document("note-get-example",
links(
linkWithRel("self").description("This <<resources-note,note>>"),
linkWithRel("tags").description("This note's tags"))
.withResponseFields(
linkWithRel("tags").description("This note's tags")),
responseFields(
fieldWithPath("title").description("The title 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
@@ -219,9 +222,9 @@ public class ApiDocumentation {
this.mockMvc.perform(get("/tags"))
.andExpect(status().isOk())
.andDo(document("tags-list-example")
.withResponseFields(
fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>")));
.andDo(document("tags-list-example",
responseFields(
fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"))));
}
@Test
@@ -233,9 +236,9 @@ public class ApiDocumentation {
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated())
.andDo(document("tags-create-example")
.withRequestFields(
fieldWithPath("name").description("The name of the tag")));
.andDo(document("tags-create-example",
requestFields(
fieldWithPath("name").description("The name of the tag"))));
}
@Test
@@ -274,12 +277,11 @@ public class ApiDocumentation {
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(noteUpdate)))
.andExpect(status().isNoContent())
.andDo(document("note-update-example")
.withRequestFields(
.andDo(document("note-update-example",
requestFields(
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("tags").description("An array of tag resource URIs").optional()));
fieldWithPath("tags").description("An array of tag resource URIs").optional())));
}
@Test
@@ -297,13 +299,13 @@ public class ApiDocumentation {
this.mockMvc.perform(get(tagLocation))
.andExpect(status().isOk())
.andExpect(jsonPath("name", is(tag.get("name"))))
.andDo(document("tag-get-example")
.withLinks(
.andDo(document("tag-get-example",
links(
linkWithRel("self").description("This <<resources-tag,tag>>"),
linkWithRel("notes").description("The <<resources-tagged-notes,notes>> that have this tag"))
.withResponseFields(
linkWithRel("notes").description("The <<resources-tagged-notes,notes>> that have this tag")),
responseFields(
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
@@ -325,9 +327,9 @@ public class ApiDocumentation {
patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tagUpdate)))
.andExpect(status().isNoContent())
.andDo(document("tag-update-example")
.withRequestFields(
fieldWithPath("name").description("The name of the tag")));
.andDo(document("tag-update-example",
requestFields(
fieldWithPath("name").description("The name of the tag"))));
}
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.documentationConfiguration;
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.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.patch;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
@@ -89,25 +92,25 @@ public class ApiDocumentation {
.andExpect(jsonPath("timestamp", is(notNullValue())))
.andExpect(jsonPath("status", is(400)))
.andExpect(jsonPath("path", is(notNullValue())))
.andDo(document("error-example")
.withResponseFields(
.andDo(document("error-example",
responseFields(
fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"),
fieldWithPath("message").description("A description of the cause of the error"),
fieldWithPath("path").description("The path to which the request was made"),
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
public void indexExample() throws Exception {
this.mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andDo(document("index-example")
.withLinks(
.andDo(document("index-example",
links(
linkWithRel("notes").description("The <<resources-notes,Notes resource>>"),
linkWithRel("tags").description("The <<resources-tags,Tags resource>>"))
.withResponseFields(
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources")));
linkWithRel("tags").description("The <<resources-tags,Tags resource>>")),
responseFields(
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources"))));
}
@Test
@@ -122,9 +125,9 @@ public class ApiDocumentation {
this.mockMvc.perform(get("/notes"))
.andExpect(status().isOk())
.andDo(document("notes-list-example")
.withResponseFields(
fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>")));
.andDo(document("notes-list-example",
responseFields(
fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"))));
}
@Test
@@ -148,11 +151,11 @@ public class ApiDocumentation {
post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated())
.andDo(document("notes-create-example")
.withRequestFields(
.andDo(document("notes-create-example",
requestFields(
fieldWithPath("title").description("The title 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
@@ -185,14 +188,14 @@ public class ApiDocumentation {
.andExpect(jsonPath("body", is(note.get("body"))))
.andExpect(jsonPath("_links.self.href", is(noteLocation)))
.andExpect(jsonPath("_links.note-tags", is(notNullValue())))
.andDo(document("note-get-example")
.withLinks(
.andDo(document("note-get-example",
links(
linkWithRel("self").description("This <<resources-note,note>>"),
linkWithRel("note-tags").description("This note's <<resources-note-tags,tags>>"))
.withResponseFields(
linkWithRel("note-tags").description("This note's <<resources-note-tags,tags>>")),
responseFields(
fieldWithPath("title").description("The title 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"))
.andExpect(status().isOk())
.andDo(document("tags-list-example")
.withResponseFields(
fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>")));
.andDo(document("tags-list-example",
responseFields(
fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"))));
}
@Test
@@ -221,9 +224,9 @@ public class ApiDocumentation {
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated())
.andDo(document("tags-create-example")
.withRequestFields(
fieldWithPath("name").description("The name of the tag")));
.andDo(document("tags-create-example",
requestFields(
fieldWithPath("name").description("The name of the tag"))));
}
@Test
@@ -262,11 +265,11 @@ public class ApiDocumentation {
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(noteUpdate)))
.andExpect(status().isNoContent())
.andDo(document("note-update-example")
.withRequestFields(
.andDo(document("note-update-example",
requestFields(
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("tags").description("An array of tag resource URIs").optional()));
fieldWithPath("tags").description("An array of tag resource URIs").optional())));
}
@Test
@@ -284,13 +287,13 @@ public class ApiDocumentation {
this.mockMvc.perform(get(tagLocation))
.andExpect(status().isOk())
.andExpect(jsonPath("name", is(tag.get("name"))))
.andDo(document("tag-get-example")
.withLinks(
.andDo(document("tag-get-example",
links(
linkWithRel("self").description("This <<resources-tag,tag>>"),
linkWithRel("tagged-notes").description("The <<resources-tagged-notes,notes>> that have this tag"))
.withResponseFields(
linkWithRel("tagged-notes").description("The <<resources-tagged-notes,notes>> that have this tag")),
responseFields(
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
@@ -312,9 +315,9 @@ public class ApiDocumentation {
patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tagUpdate)))
.andExpect(status().isNoContent())
.andDo(document("tag-update-example")
.withRequestFields(
fieldWithPath("name").description("The name of the tag")));
.andDo(document("tag-update-example",
requestFields(
fieldWithPath("name").description("The name of the tag"))));
}
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.mock.web.MockHttpServletResponse;
import org.springframework.restdocs.response.ResponsePostProcessor;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.ReflectionUtils;
@@ -47,19 +48,22 @@ public final class ResponseModifier {
/**
* Provides a {@link RestDocumentationResultHandler} that can be used to document the
* request and modified result.
* @param identifier An identifier for the API call that is being documented
* request and modified response.
* @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
*/
public RestDocumentationResultHandler andDocument(String identifier) {
return new ResponseModifyingRestDocumentationResultHandler(identifier);
public RestDocumentationResultHandler andDocument(String identifier,
Snippet... snippets) {
return new ResponseModifyingRestDocumentationResultHandler(identifier, snippets);
}
class ResponseModifyingRestDocumentationResultHandler extends
RestDocumentationResultHandler {
public ResponseModifyingRestDocumentationResultHandler(String identifier) {
super(identifier);
private ResponseModifyingRestDocumentationResultHandler(String identifier,
Snippet... snippets) {
super(identifier, snippets);
}
@Override

View File

@@ -19,6 +19,7 @@ package org.springframework.restdocs;
import org.springframework.restdocs.config.RestDocumentationConfigurer;
import org.springframework.restdocs.response.ResponsePostProcessor;
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.MvcResult;
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
* REST documentation when building a {@link MockMvc} instance.
*
* @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer)
* @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
* @see MockMvc#perform(org.springframework.test.web.servlet.RequestBuilder)
* @see ResultActions#andDo(org.springframework.test.web.servlet.ResultHandler)
*/
public static RestDocumentationResultHandler document(String identifier) {
return new RestDocumentationResultHandler(identifier);
public static RestDocumentationResultHandler document(String identifier,
Snippet... snippets) {
return new RestDocumentationResultHandler(identifier, snippets);
}
/**

View File

@@ -19,6 +19,7 @@ package org.springframework.restdocs;
import java.net.URI;
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.MockMultipartHttpServletRequestBuilder;
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
* template and makes it available for documentation. Required when
* {@link RestDocumentationResultHandler#withPathParameters(org.springframework.restdocs .request.ParameterDescriptor...)
* documenting path parameters} and recommended for general usage.
* {@link RequestDocumentation#pathParameters(org.springframework.restdocs.request.ParameterDescriptor...)
* ) documenting path parameters} and recommended for general usage.
*
* @author Andy Wilkinson
* @see MockMvcRequestBuilders
* @see RestDocumentationResultHandler#withPathParameters(org.springframework.restdocs.request.ParameterDescriptor...)
* @see RestDocumentationResultHandler#withPathParameters(java.util.Map,
* @see RequestDocumentation#pathParameters(org.springframework.restdocs.request.ParameterDescriptor...)
* @see RequestDocumentation#pathParameters(java.util.Map,
* org.springframework.restdocs.request.ParameterDescriptor...)
*/
public abstract class RestDocumentationRequestBuilders {

View File

@@ -16,28 +16,11 @@
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.Arrays;
import java.util.List;
import java.util.Map;
import org.springframework.restdocs.hypermedia.HypermediaDocumentation;
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.restdocs.snippet.Snippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
@@ -46,317 +29,32 @@ import org.springframework.test.web.servlet.ResultHandler;
*
* @author Andy Wilkinson
* @author Andreas Evers
* @see RestDocumentation#document(String)
* @see RestDocumentation#document(String, Snippet...)
*/
public class RestDocumentationResultHandler implements ResultHandler {
private final String identifier;
private SnippetWritingResultHandler curlRequest;
private final List<Snippet> snippets;
private SnippetWritingResultHandler httpRequest;
private SnippetWritingResultHandler httpResponse;
private List<SnippetWritingResultHandler> delegates = new ArrayList<>();
RestDocumentationResultHandler(String identifier) {
RestDocumentationResultHandler(String identifier, Snippet... snippets) {
this.identifier = identifier;
this.curlRequest = documentCurlRequest(this.identifier, null);
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;
this.snippets = Arrays.asList(snippets);
}
@Override
public void handle(MvcResult result) throws Exception {
this.curlRequest.handle(result);
this.httpRequest.handle(result);
this.httpResponse.handle(result);
for (ResultHandler delegate : this.delegates) {
delegate.handle(result);
for (Snippet snippet : getSnippets(result)) {
snippet.document(this.identifier, 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 {
/**
* 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
*/
abstract void apply(MockHttpServletRequest request);

View File

@@ -49,9 +49,9 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter {
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}.
@@ -65,19 +65,44 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter {
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() {
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() {
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) {
this.templateEngineConfigurer.setTemplateEngine(templateEngine);
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) {
this.writerResolverConfigurer.setWriterResolver(writerResolver);
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;
public RestDocumentationContext() {
this(null);
}
/**
* Creates a new {@code RestDocumentationContext} backed by the given
* {@code testContext}.
*
* @param testContext the test context
*/
public RestDocumentationContext(TestContext testContext) {
this.testContext = testContext;
}

View File

@@ -46,6 +46,12 @@ public class RestDocumentationContextPlaceholderResolver implements PlaceholderR
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) {
this.context = context;
}

View File

@@ -16,7 +16,13 @@
package org.springframework.restdocs.config;
import java.util.Arrays;
import java.util.List;
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;
/**
@@ -28,6 +34,10 @@ import org.springframework.restdocs.snippet.WriterResolver;
public class SnippetConfigurer extends
AbstractNestedConfigurer<RestDocumentationConfigurer> {
private List<Snippet> defaultSnippets = Arrays.asList(
CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(),
HttpDocumentation.httpResponse());
/**
* The default encoding for documentation snippets
* @see #withEncoding(String)
@@ -43,7 +53,7 @@ public class SnippetConfigurer extends
/**
* Configures any documentation snippets to be written using the given
* {@code encoding}. The default is UTF-8.
* @param encoding The encoding
* @param encoding the encoding
* @return {@code this}
*/
public SnippetConfigurer withEncoding(String encoding) {
@@ -55,5 +65,18 @@ public class SnippetConfigurer extends
void apply(MockHttpServletRequest request) {
((WriterResolver) request.getAttribute(WriterResolver.class.getName()))
.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;
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.SnippetWritingResultHandler;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.restdocs.snippet.Snippet;
/**
* 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
* snippet
* @return the handler that will produce the snippet
*/
public static SnippetWritingResultHandler documentCurlRequest(String identifier,
Map<String, Object> 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));
}
}
}
public static Snippet curlRequest(Map<String, Object> attributes) {
return new CurlRequestSnippet(attributes);
}
}

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;
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.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;
import org.springframework.restdocs.snippet.Snippet;
/**
* 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 {
private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
private HttpDocumentation() {
}
/**
* Produces a documentation snippet containing the request formatted as an HTTP
* request
* Returns a handler that will produce a snippet containing the HTTP request for the
* 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
*/
public static SnippetWritingResultHandler documentHttpRequest(String identifier,
Map<String, Object> attributes) {
return new HttpRequestWritingResultHandler(identifier, attributes);
public static Snippet httpRequest() {
return new HttpRequestSnippet();
}
/**
* Produces a documentation snippet containing the response formatted as the HTTP
* response sent by the server
*
* @param identifier An identifier for the API call that is being documented
* @param attributes Attributes made available during rendering of the HTTP response
* snippet
* Returns a handler that will produce a snippet containing the HTTP request for the
* API call. The given {@code attributes} will be available during snippet generation.
*
* @param attributes the attributes
* @return the handler that will produce the snippet
*/
public static SnippetWritingResultHandler documentHttpResponse(String identifier,
Map<String, Object> attributes) {
return new HttpResponseWritingResultHandler(identifier, attributes);
public static Snippet httpRequest(Map<String, Object> attributes) {
return new HttpRequestSnippet(attributes);
}
private static final class HttpRequestWritingResultHandler extends
SnippetWritingResultHandler {
private HttpRequestWritingResultHandler(String identifier,
Map<String, Object> attributes) {
super(identifier, "http-request", attributes);
}
@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;
}
/**
* Returns a handler that will produce a snippet containing the HTTP response for the
* API call.
* @return the handler that will produce the snippet
*/
public static Snippet httpResponse() {
return new HttpResponseSnippet();
}
private static final class HttpResponseWritingResultHandler extends
SnippetWritingResultHandler {
private HttpResponseWritingResultHandler(String identifier,
Map<String, Object> attributes) {
super(identifier, "http-response", attributes);
}
@Override
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;
}
/**
* 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.
*
* @param attributes the attributes
* @return the handler that will produce the snippet
*/
public static Snippet httpResponse(Map<String, Object> attributes) {
return new HttpResponseSnippet(attributes);
}
}

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.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
*/
@@ -37,29 +37,89 @@ public abstract class HypermediaDocumentation {
*
* @param rel The rel of the link
* @return a {@code LinkDescriptor} ready for further configuration
* @see RestDocumentationResultHandler#withLinks(LinkDescriptor...)
* @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...)
*/
public static LinkDescriptor linkWithRel(String rel) {
return new LinkDescriptor(rel);
}
/**
* Creates a {@code LinkSnippetResultHandler} that will produce a documentation
* snippet for a response's links.
* Returns a handler that will produce a snippet documenting the links in the API
* 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 linkExtractor Used to extract the links from the response
* @param descriptors The descriptions of the response's links
* @return the handler
* @see RestDocumentationResultHandler#withLinks(LinkDescriptor...)
* @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...)
*/
public static LinkSnippetResultHandler documentLinks(String identifier,
Map<String, Object> attributes, LinkExtractor linkExtractor,
LinkDescriptor... descriptors) {
return new LinkSnippetResultHandler(identifier, attributes, linkExtractor,
public static Snippet links(LinkExtractor linkExtractor,
Map<String, Object> attributes, LinkDescriptor... descriptors) {
return new LinksSnippet(linkExtractor, attributes,
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.Set;
import org.springframework.restdocs.snippet.SnippetGenerationException;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert;
/**
* A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful
* resource's links.
* A {@link Snippet} that documents a RESTful resource's links.
*
* @author Andy Wilkinson
*/
public class LinkSnippetResultHandler extends SnippetWritingResultHandler {
class LinksSnippet extends TemplatedSnippet {
private final Map<String, LinkDescriptor> descriptorsByRel = new LinkedHashMap<>();
private final Set<String> requiredRels = new HashSet<>();
private final LinkExtractor extractor;
private final LinkExtractor linkExtractor;
LinkSnippetResultHandler(String identifier, Map<String, Object> attributes,
LinkExtractor linkExtractor, List<LinkDescriptor> descriptors) {
super(identifier, "links", attributes);
this.extractor = linkExtractor;
LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descriptors) {
this(linkExtractor, null, descriptors);
}
LinksSnippet(LinkExtractor linkExtractor, Map<String, Object> attributes,
List<LinkDescriptor> descriptors) {
super("links", attributes);
this.linkExtractor = linkExtractor;
for (LinkDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getRel());
Assert.hasText(descriptor.getDescription());
@@ -60,31 +64,13 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler {
}
@Override
protected Map<String, Object> doHandle(MvcResult result) throws IOException {
validate(extractLinks(result));
protected Map<String, Object> document(MvcResult result) throws IOException {
validate(this.linkExtractor.extractLinks(result.getResponse()));
Map<String, Object> model = new HashMap<>();
model.put("links", createLinksModel());
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) {
Set<String> actualRels = links.keySet();
@@ -104,10 +90,10 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler {
if (message.length() > 0) {
message += ". ";
}
message += "Links with the following relations were not found in the response: "
+ missingRels;
message += "Links with the following relations were not found in the "
+ "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.Entry;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.util.Assert;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful
* resource's request or response fields.
* A {@link TemplatedSnippet} that produces a snippet documenting a
* RESTful resource's request or response fields.
*
* @author Andreas Evers
* @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>();
@@ -50,9 +51,9 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
private List<FieldDescriptor> fieldDescriptors;
FieldSnippetResultHandler(String identifier, String type,
Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
super(identifier, type + "-fields", attributes);
AbstractFieldsSnippet(String type, Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
super(type + "-fields", attributes);
for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath());
Assert.hasText(descriptor.getDescription());
@@ -62,7 +63,7 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
}
@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);
Object payload = extractPayload(result);
Map<String, Object> model = new HashMap<>();
@@ -80,8 +81,8 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
private FieldType getFieldType(FieldDescriptor descriptor, Object payload) {
try {
return FieldSnippetResultHandler.this.fieldTypeResolver.resolveFieldType(
descriptor.getPath(), payload);
return AbstractFieldsSnippet.this.fieldTypeResolver
.resolveFieldType(descriptor.getPath(), payload);
}
catch (FieldDoesNotExistException ex) {
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.Map;
import org.springframework.restdocs.snippet.SnippetGenerationException;
import org.springframework.restdocs.snippet.SnippetException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -60,7 +60,7 @@ class FieldValidator {
message += "Fields with the following paths were not found in the payload: "
+ 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.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.
@@ -89,57 +89,92 @@ public abstract class PayloadDocumentation {
*
* @param path The path of the field
* @return a {@code FieldDescriptor} ready for further configuration
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
*/
public static FieldDescriptor fieldWithPath(String path) {
return new FieldDescriptor(path);
}
/**
* Creates a {@code RequestFieldsSnippetResultHandler} that will produce a
* documentation snippet for a request's fields.
* Returns a handler that will produce a snippet documenting the fields of the API
* call's request.
* <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
* 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 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
* @return the handler
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
* @see #fieldWithPath(String)
*/
public static FieldSnippetResultHandler documentRequestFields(String identifier,
Map<String, Object> attributes, FieldDescriptor... descriptors) {
return new RequestFieldSnippetResultHandler(identifier, attributes,
public static Snippet requestFields(FieldDescriptor... descriptors) {
return new RequestFieldsSnippet(Arrays.asList(descriptors));
}
/**
* 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));
}
/**
* Creates a {@code ResponseFieldsSnippetResultHandler} that will produce a
* documentation snippet for a response's fields.
* Returns a handler that will produce a snippet documenting the fields of the API
* call's response.
* <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
* 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 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
* @return the handler
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
* @see #fieldWithPath(String)
*/
public static FieldSnippetResultHandler documentResponseFields(String identifier,
Map<String, Object> attributes, FieldDescriptor... descriptors) {
return new ResponseFieldSnippetResultHandler(identifier, attributes,
public static Snippet responseFields(FieldDescriptor... descriptors) {
return new ResponseFieldsSnippet(Arrays.asList(descriptors));
}
/**
* 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));
}

View File

@@ -20,18 +20,22 @@ import java.io.Reader;
import java.util.List;
import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
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
*/
public class RequestFieldSnippetResultHandler extends FieldSnippetResultHandler {
class RequestFieldsSnippet extends AbstractFieldsSnippet {
RequestFieldSnippetResultHandler(String identifier, Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
super(identifier, "request", attributes, descriptors);
RequestFieldsSnippet(List<FieldDescriptor> descriptors) {
this(null, descriptors);
}
RequestFieldsSnippet(Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
super("request", attributes, descriptors);
}
@Override

View File

@@ -21,18 +21,23 @@ import java.io.StringReader;
import java.util.List;
import java.util.Map;
import org.springframework.restdocs.snippet.Snippet;
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
*/
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) {
super(identifier, "response", attributes, descriptors);
super("response", attributes, descriptors);
}
@Override

View File

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

View File

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

View File

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

View File

@@ -16,10 +16,10 @@
package org.springframework.restdocs.request;
import java.util.Arrays;
import java.util.Map;
import org.springframework.restdocs.RestDocumentationResultHandler;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.restdocs.snippet.Snippet;
/**
* 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
* the given {@code name}.
*
* @param name The name of the parameter
* @return a {@link ParameterDescriptor} ready for further configuration
* @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...)
*/
public static ParameterDescriptor parameterWithName(String 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;
public ContentModifyingMethodInterceptor(String modifiedContent,
private ContentModifyingMethodInterceptor(String modifiedContent,
MockHttpServletResponse delegate) {
this.modifiedContent = modifiedContent;
this.delegate = delegate;

View File

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

View File

@@ -75,12 +75,11 @@ public abstract class Attributes {
public Attribute value(Object value) {
return new Attribute(this.key, value);
}
}
/**
* An attribute (key-value pair).
*
* @author Andy Wilkinson
*/
public static class Attribute {
@@ -88,18 +87,32 @@ public abstract class Attributes {
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) {
this.key = key;
this.value = value;
}
/**
* Returns the attribute's key
* @return the key
*/
public String getKey() {
return this.key;
}
/**
* Returns the attribute's value
* @return the value
*/
public Object getValue() {
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
*/
@SuppressWarnings("serial")
public class SnippetGenerationException extends RuntimeException {
public class SnippetException extends RuntimeException {
/**
* Creates a new {@code SnippetGenerationException} described by the given
* {@code message}
* Creates a new {@code SnippetException} described by the given {@code message}
* @param message the message that describes the problem
*/
public SnippetGenerationException(String message) {
public SnippetException(String message) {
super(message);
}

View File

@@ -21,26 +21,23 @@ import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import org.springframework.restdocs.templates.Template;
import org.springframework.restdocs.templates.TemplateEngine;
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
*/
public abstract class SnippetWritingResultHandler implements ResultHandler {
public abstract class TemplatedSnippet implements Snippet {
private final Map<String, Object> attributes = new HashMap<>();
private final String identifier;
private final String snippetName;
protected SnippetWritingResultHandler(String identifier, String snippetName,
Map<String, Object> attributes) {
this.identifier = identifier;
protected TemplatedSnippet(String snippetName, Map<String, Object> attributes) {
this.snippetName = snippetName;
if (attributes != null) {
this.attributes.putAll(attributes);
@@ -48,11 +45,11 @@ public abstract class SnippetWritingResultHandler implements ResultHandler {
}
@Override
public void handle(MvcResult result) throws IOException {
public void document(String operation, MvcResult result) throws IOException {
WriterResolver writerResolver = (WriterResolver) result.getRequest()
.getAttribute(WriterResolver.class.getName());
try (Writer writer = writerResolver.resolve(this.identifier, this.snippetName)) {
Map<String, Object> model = doHandle(result);
try (Writer writer = writerResolver.resolve(operation, this.snippetName)) {
Map<String, Object> model = document(result);
model.putAll(this.attributes);
TemplateEngine templateEngine = (TemplateEngine) result.getRequest()
.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.modifyResponseTo;
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.links;
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.pathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.restdocs.response.ResponsePostProcessors.maskLinks;
import static org.springframework.restdocs.response.ResponsePostProcessors.prettyPrintContent;
import static org.springframework.restdocs.response.ResponsePostProcessors.removeHeaders;
@@ -112,14 +118,14 @@ public class RestDocumentationIntegrationTests {
}
@Test
public void links() throws Exception {
public void linksSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build();
mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("links").withLinks(
linkWithRel("rel").description("The description")));
.andDo(document("links",
links(linkWithRel("rel").description("The description"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -127,14 +133,14 @@ public class RestDocumentationIntegrationTests {
}
@Test
public void pathParameters() throws Exception {
public void pathParametersSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build();
mockMvc.perform(get("{foo}", "/").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("links").withPathParameters(
parameterWithName("foo").description("The description")));
.andDo(document("links", pathParameters(parameterWithName("foo")
.description("The description"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -142,14 +148,14 @@ public class RestDocumentationIntegrationTests {
}
@Test
public void queryParameters() throws Exception {
public void queryParametersSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build();
mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("links").withQueryParameters(
parameterWithName("foo").description("The description")));
.andDo(document("links", queryParameters(parameterWithName("foo")
.description("The description"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -157,7 +163,7 @@ public class RestDocumentationIntegrationTests {
}
@Test
public void requestFields() throws Exception {
public void requestFieldsSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build();
@@ -165,8 +171,8 @@ public class RestDocumentationIntegrationTests {
get("/").param("foo", "bar").content("{\"a\":\"alpha\"}")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("links").withRequestFields(
fieldWithPath("a").description("The description")));
.andDo(document("links",
requestFields(fieldWithPath("a").description("The description"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -174,15 +180,18 @@ public class RestDocumentationIntegrationTests {
}
@Test
public void responseFields() throws Exception {
public void responseFieldsSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(new RestDocumentationConfigurer()).build();
mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("links").withResponseFields(
fieldWithPath("a").description("The description"),
fieldWithPath("links").description("Links to other resources")));
.andDo(document(
"links",
responseFields(
fieldWithPath("a").description("The description"),
fieldWithPath("links").description(
"Links to other resources"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc",
@@ -284,8 +293,10 @@ public class RestDocumentationIntegrationTests {
is(snippet().withContents(equalTo("Custom curl request"))));
mockMvc.perform(get("/")).andDo(
document("index").withCurlRequest(
attributes(key("title").value("Access the index using curl"))));
document(
"index",
curlRequest(attributes(key("title").value(
"Access the index using curl")))));
}
private void assertExpectedSnippetFilesExist(File directory, String... snippets) {

View File

@@ -43,7 +43,7 @@ public class RestDocumentationConfigurerTests {
private MockHttpServletRequest request = new MockHttpServletRequest();
private RestDocumentationContext context = new RestDocumentationContext();
private RestDocumentationContext context = new RestDocumentationContext(null);
@Before
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.post;
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.key;
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;
/**
* Tests for {@link CurlDocumentation}
* Tests for {@link CurlRequestSnippet}
*
* @author Andy Wilkinson
* @author Yann Le Guern
* @author Dmitriy Mayboroda
* @author Jonathan Pearlin
*/
public class CurlDocumentationTests {
public class CurlRequestDocumentationHandlerTests {
@Rule
public ExpectedSnippet snippet = new ExpectedSnippet();
@@ -63,14 +62,16 @@ public class CurlDocumentationTests {
public void getRequest() throws IOException {
this.snippet.expectCurlRequest("get-request").withContents(
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
public void nonGetRequest() throws IOException {
this.snippet.expectCurlRequest("non-get-request").withContents(
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
@@ -78,7 +79,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("request-with-content").withContents(
codeBlock("bash")
.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")));
}
@@ -88,7 +89,7 @@ public class CurlDocumentationTests {
.withContents(
codeBlock("bash").content(
"$ 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")));
}
@@ -96,7 +97,7 @@ public class CurlDocumentationTests {
public void requestWithOneParameter() throws IOException {
this.snippet.expectCurlRequest("request-with-one-parameter").withContents(
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")));
}
@@ -105,9 +106,9 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i"));
documentCurlRequest("request-with-multiple-parameters", null).handle(
result(get("/foo").param("k1", "v1").param("k2", "v2")
.param("k1", "v1-bis")));
new CurlRequestSnippet().document(
"request-with-multiple-parameters", result(get("/foo").param("k1", "v1")
.param("k2", "v2").param("k1", "v1-bis")));
}
@Test
@@ -116,7 +117,8 @@ public class CurlDocumentationTests {
.withContents(
codeBlock("bash").content(
"$ 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&")));
}
@@ -125,7 +127,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents(
codeBlock("bash").content(
"$ 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")));
}
@@ -136,7 +138,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST"
+ " -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")));
}
@@ -147,7 +150,8 @@ public class CurlDocumentationTests {
.withContents(
codeBlock("bash").content(
"$ 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")));
}
@@ -156,7 +160,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents(
codeBlock("bash").content(
"$ 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")));
}
@@ -167,7 +171,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT"
+ " -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")));
}
@@ -177,7 +182,8 @@ public class CurlDocumentationTests {
.withContents(
codeBlock("bash").content(
"$ 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")));
}
@@ -187,7 +193,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i"
+ " -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",
"alpha")));
}
@@ -198,7 +205,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(8080);
documentCurlRequest("http-with-non-standard-port", null).handle(result(request));
new CurlRequestSnippet().document("http-with-non-standard-port",
result(request));
}
@Test
@@ -208,7 +216,8 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(443);
request.setScheme("https");
documentCurlRequest("https-with-standard-port", null).handle(result(request));
new CurlRequestSnippet().document("https-with-standard-port",
result(request));
}
@Test
@@ -218,7 +227,8 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(8443);
request.setScheme("https");
documentCurlRequest("https-with-non-standard-port", null).handle(result(request));
new CurlRequestSnippet().document("https-with-non-standard-port",
result(request));
}
@Test
@@ -227,7 +237,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
documentCurlRequest("request-with-custom-host", null).handle(result(request));
new CurlRequestSnippet().document("request-with-custom-host",
result(request));
}
@Test
@@ -239,8 +250,8 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
request.setContextPath("/v3");
documentCurlRequest("request-with-custom-context-with-slash", null).handle(
result(request));
new CurlRequestSnippet().document(
"request-with-custom-context-with-slash", result(request));
}
@Test
@@ -252,8 +263,8 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
request.setContextPath("v3");
documentCurlRequest("request-with-custom-context-without-slash", null).handle(
result(request));
new CurlRequestSnippet().document(
"request-with-custom-context-without-slash", result(request));
}
@Test
@@ -265,7 +276,8 @@ public class CurlDocumentationTests {
.withContents(codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("metadata",
"{\"description\": \"foo\"}".getBytes());
documentCurlRequest("multipart-post-no-original-filename", null).handle(
new CurlRequestSnippet().document(
"multipart-post-no-original-filename",
result(fileUpload("/upload").file(multipartFile)));
}
@@ -279,7 +291,8 @@ public class CurlDocumentationTests {
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", MediaType.IMAGE_PNG_VALUE,
"bytes".getBytes());
documentCurlRequest("multipart-post-with-content-type", null).handle(
new CurlRequestSnippet().document(
"multipart-post-with-content-type",
result(fileUpload("/upload").file(multipartFile)));
}
@@ -292,7 +305,7 @@ public class CurlDocumentationTests {
codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "bytes".getBytes());
documentCurlRequest("multipart-post", null).handle(
new CurlRequestSnippet().document("multipart-post",
result(fileUpload("/upload").file(multipartFile)));
}
@@ -306,7 +319,8 @@ public class CurlDocumentationTests {
codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "bytes".getBytes());
documentCurlRequest("multipart-post", null).handle(
new CurlRequestSnippet().document(
"multipart-post",
result(fileUpload("/upload").file(multipartFile)
.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"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentCurlRequest("custom-attributes",
attributes(key("title").value("curl request title"))).handle(
result(request));
new CurlRequestSnippet(attributes(key("title").value(
"curl request title"))).document("custom-attributes", result(request));
}
}

View File

@@ -19,18 +19,13 @@ 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.RestDocumentationRequestBuilders.fileUpload;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.RestDocumentationRequestBuilders.post;
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.key;
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.web.bind.annotation.RequestMethod.GET;
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.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
@@ -52,12 +46,13 @@ import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
* Tests for {@link HttpDocumentation}
*
* Tests for {@link HttpRequestSnippet}
*
* @author Andy Wilkinson
* @author Jonathan Pearlin
*
*/
public class HttpDocumentationTests {
public class HttpRequestDocumentationHandlerTests {
private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
@@ -70,8 +65,8 @@ public class HttpDocumentationTests {
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header(
"Alpha", "a"));
documentHttpRequest("get-request", null).handle(
result(get("/foo").header("Alpha", "a")));
new HttpRequestSnippet().document("get-request", result(get("/foo")
.header("Alpha", "a")));
}
@Test
@@ -79,7 +74,7 @@ public class HttpDocumentationTests {
this.snippet.expectHttpRequest("get-request-with-query-string").withContents(
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")));
}
@@ -88,7 +83,7 @@ public class HttpDocumentationTests {
this.snippet.expectHttpRequest("get-request-with-parameter").withContents(
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")));
}
@@ -98,7 +93,7 @@ public class HttpDocumentationTests {
httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world"));
documentHttpRequest("post-request-with-content", null).handle(
new HttpRequestSnippet().document("post-request-with-content",
result(post("/foo").content("Hello, world")));
}
@@ -109,7 +104,7 @@ public class HttpDocumentationTests {
.header("Content-Type", "application/x-www-form-urlencoded")
.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")));
}
@@ -119,7 +114,7 @@ public class HttpDocumentationTests {
httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world"));
documentHttpRequest("put-request-with-content", null).handle(
new HttpRequestSnippet().document("put-request-with-content",
result(put("/foo").content("Hello, world")));
}
@@ -130,48 +125,10 @@ public class HttpDocumentationTests {
.header("Content-Type", "application/x-www-form-urlencoded")
.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")));
}
@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
public void multipartPost() throws IOException {
String expectedContent = createPart(String.format("Content-Disposition: "
@@ -184,7 +141,7 @@ public class HttpDocumentationTests {
.content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "<< data >>".getBytes());
documentHttpRequest("multipart-post", null).handle(
new HttpRequestSnippet().document("multipart-post",
result(fileUpload("/upload").file(multipartFile)));
}
@@ -207,7 +164,8 @@ public class HttpDocumentationTests {
.content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "<< data >>".getBytes());
documentHttpRequest("multipart-post", null).handle(
new HttpRequestSnippet().document(
"multipart-post",
result(fileUpload("/upload").file(multipartFile)
.param("a", "apple", "avocado").param("b", "banana")));
}
@@ -226,7 +184,8 @@ public class HttpDocumentationTests {
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", MediaType.IMAGE_PNG_VALUE,
"<< data >>".getBytes());
documentHttpRequest("multipart-post-with-content-type", null).handle(
new HttpRequestSnippet().document(
"multipart-post-with-content-type",
result(fileUpload("/upload").file(multipartFile)));
}
@@ -238,7 +197,7 @@ public class HttpDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
documentHttpRequest("get-request-custom-server-name", null).handle(
new HttpRequestSnippet().document("get-request-custom-server-name",
result(request));
}
@@ -247,7 +206,7 @@ public class HttpDocumentationTests {
this.snippet.expectHttpRequest("get-request-custom-host").withContents(
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")));
}
@@ -263,25 +222,8 @@ public class HttpDocumentationTests {
"src/test/resources/custom-snippet-templates/http-request-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentHttpRequest("request-with-snippet-attributes",
attributes(key("title").value("Title for the request"))).handle(
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(
new HttpRequestSnippet(attributes(key("title").value(
"Title for the request"))).document("request-with-snippet-attributes",
result(request));
}
@@ -290,12 +232,12 @@ public class HttpDocumentationTests {
}
private String createPart(String content, boolean last) {
String boundary = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
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) {
part.append(String.format("--%s--", boundary));
part.append(String.format("--%s--", BOUNDARY));
}
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;
/**
* Parameterized tests for {@link LinkExtractors} with various payloads.
* Parameterized tests for {@link HalLinkExtractor} and {@link AtomLinkExtractor} with
* various payloads.
*
* @author Andy Wilkinson
*/
@@ -50,8 +51,8 @@ public class LinkExtractorsPayloadTests {
@Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[] { LinkExtractors.halLinks(), "hal" },
new Object[] { LinkExtractors.atomLinks(), "atom" });
return Arrays.asList(new Object[] { new HalLinkExtractor(), "hal" },
new Object[] { new AtomLinkExtractor(), "atom" });
}
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.mockito.Mockito.mock;
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.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;
@@ -34,7 +35,7 @@ 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.snippet.SnippetException;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
@@ -43,11 +44,11 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Tests for {@link HypermediaDocumentation}
* Tests for {@link LinksSnippet}
*
* @author Andy Wilkinson
*/
public class HypermediaDocumentationTests {
public class LinksDocumentationHandlerTests {
@Rule
public ExpectedSnippet snippet = new ExpectedSnippet();
@@ -57,21 +58,22 @@ public class HypermediaDocumentationTests {
@Test
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"
+ " documented: [foo]"));
documentLinks("undocumented-link", null,
new StubLinkExtractor().withLinks(new Link("foo", "bar"))).handle(
result());
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo",
"bar")), Collections.<LinkDescriptor> emptyList()).document(
"undocumented-link", result());
}
@Test
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"
+ " found in the response: [foo]"));
documentLinks("missing-link", null, new StubLinkExtractor(),
new LinkDescriptor("foo").description("bar")).handle(result());
new LinksSnippet(new StubLinkExtractor(),
Arrays.asList(new LinkDescriptor("foo").description("bar"))).document(
"missing-link", result());
}
@Test
@@ -79,9 +81,9 @@ public class HypermediaDocumentationTests {
this.snippet.expectLinks("documented-optional-link").withContents( //
tableWithHeader("Relation", "Description") //
.row("foo", "bar"));
documentLinks("documented-optional-link", null,
new StubLinkExtractor().withLinks(new Link("foo", "blah")),
new LinkDescriptor("foo").description("bar").optional()).handle(result());
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo",
"blah")), Arrays.asList(new LinkDescriptor("foo").description("bar")
.optional())).document("documented-optional-link", result());
}
@Test
@@ -89,19 +91,20 @@ public class HypermediaDocumentationTests {
this.snippet.expectLinks("missing-optional-link").withContents( //
tableWithHeader("Relation", "Description") //
.row("foo", "bar"));
documentLinks("missing-optional-link", null, new StubLinkExtractor(),
new LinkDescriptor("foo").description("bar").optional()).handle(result());
new LinksSnippet(new StubLinkExtractor(),
Arrays.asList(new LinkDescriptor("foo").description("bar").optional()))
.document("missing-optional-link", result());
}
@Test
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"
+ " documented: [a]. Links with the following relations were not"
+ " found in the response: [foo]"));
documentLinks("undocumented-link-and-missing-link", null,
new StubLinkExtractor().withLinks(new Link("a", "alpha")),
new LinkDescriptor("foo").description("bar")).handle(result());
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"alpha")), Arrays.asList(new LinkDescriptor("foo").description("bar")))
.document("undocumented-link-and-missing-link", result());
}
@Test
@@ -110,12 +113,11 @@ public class HypermediaDocumentationTests {
tableWithHeader("Relation", "Description") //
.row("a", "one") //
.row("b", "two"));
documentLinks(
"documented-links",
null,
new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b",
"bravo")), new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two")).handle(result());
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"alpha"), new Link("b", "bravo")), Arrays.asList(
new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two"))).document("documented-links",
result());
}
@Test
@@ -132,20 +134,18 @@ public class HypermediaDocumentationTests {
"src/test/resources/custom-snippet-templates/links-with-extra-column.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentLinks(
"links-with-custom-descriptor-attributes",
null,
new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b",
"bravo")),
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"alpha"), new Link("b", "bravo")), Arrays.asList(
new LinkDescriptor("a").description("one").attributes(
key("foo").value("alpha")),
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
public void linksWithCustomAttribute() throws IOException {
this.snippet.expectLinks("links-with-custom-attribute").withContents(
public void linksWithCustomAttributes() throws IOException {
this.snippet.expectLinks("links-with-custom-attributes").withContents(
startsWith(".Title for the links"));
MockHttpServletRequest request = new MockHttpServletRequest();
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
@@ -155,12 +155,12 @@ public class HypermediaDocumentationTests {
"src/test/resources/custom-snippet-templates/links-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentLinks(
"links-with-custom-attribute",
attributes(key("title").value("Title for the links")),
new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b",
"bravo")), new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two")).handle(result(request));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a",
"alpha"), new Link("b", "bravo")), attributes(key("title").value(
"Title for the links")), Arrays.asList(
new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two"))).document(
"links-with-custom-attributes", result(request));
}
private static class StubLinkExtractor implements LinkExtractor {

View File

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

View File

@@ -26,12 +26,12 @@ import org.hamcrest.Matcher;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
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;
/**
* 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
*/

View File

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

View File

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