From 5da4bee3c6f281398c229fdca06541ec07e0cc8a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 28 Sep 2015 11:14:58 +0100 Subject: [PATCH 001/898] Improve handling of the Content-Length header Previously, some MockMvc-specific logic would add a Content-Length header to every request that had content. This led to the curl request snippet containing a -H option for the Content-Length header. This is unnecessary as curl will automatically generate a Content-Length header based on the data that's being sent to the server. A secondary problem was the inconsistent automatic addition of a Content-Length header; the header was not automatically added to responses. This commit remove the MockMvc-specific logic in favour of some new logic in the core project to automatically add a Content-Length header to both requests and responses. The curl request snippet has been updated to supress the header in favour of curl's automatic generation. Closes gh-111 --- .../restdocs/curl/CurlRequestSnippet.java | 6 ++-- .../operation/AbstractOperationMessage.java | 13 ++++++- ...ContentModifyingOperationPreprocessor.java | 5 ++- .../http/HttpRequestSnippetTests.java | 24 +++++++------ .../http/HttpResponseSnippetTests.java | 17 +++++---- ...ntModifyingOperationPreprocessorTests.java | 10 ------ .../restdocs/test/SnippetMatchers.java | 6 ++++ .../RestDocumentationMockMvcConfigurer.java | 17 +-------- .../MockMvcOperationRequestFactoryTests.java | 3 +- ...kMvcRestDocumentationIntegrationTests.java | 35 ++++++++++++++----- .../RestDocumentationConfigurerTests.java | 12 ------- 11 files changed, 81 insertions(+), 67 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 57cddd6c2..4d1ce4461 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -115,8 +115,10 @@ private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter wr private void writeHeaders(HttpHeaders headers, PrintWriter writer) { for (Entry> entry : headers.entrySet()) { - for (String header : entry.getValue()) { - writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); + if (!HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(entry.getKey())) { + for (String header : entry.getValue()) { + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); + } } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java index c5ede617d..d111f321b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -35,7 +35,18 @@ abstract class AbstractOperationMessage { AbstractOperationMessage(byte[] content, HttpHeaders headers) { this.content = content == null ? new byte[0] : content; - this.headers = headers; + this.headers = createHeaders(content, headers); + } + + private static HttpHeaders createHeaders(byte[] content, HttpHeaders input) { + HttpHeaders headers = new HttpHeaders(); + if (input != null) { + headers.putAll(input); + } + if (content != null && content.length > 0 && headers.getContentLength() == -1) { + headers.setContentLength(content.length); + } + return headers; } public byte[] getContent() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index ad37baea1..5cc31561b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -63,9 +63,12 @@ public OperationResponse preprocess(OperationResponse response) { private HttpHeaders getUpdatedHeaders(HttpHeaders headers, byte[] updatedContent) { HttpHeaders updatedHeaders = new HttpHeaders(); updatedHeaders.putAll(headers); - if (updatedHeaders.getContentLength() > -1) { + if (updatedContent.length > 0) { updatedHeaders.setContentLength(updatedContent.length); } + else { + updatedHeaders.remove(HttpHeaders.CONTENT_LENGTH); + } return updatedHeaders; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 83ce80ba9..9ccb89f1d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -75,30 +75,33 @@ public void getRequestWithQueryString() throws IOException { @Test public void postRequestWithContent() throws IOException { + String content = "Hello, world"; this.snippet.expectHttpRequest("post-request-with-content").withContents( - httpRequest(RequestMethod.POST, "/foo").header(HttpHeaders.HOST, - "localhost").content("Hello, world")); + httpRequest(RequestMethod.POST, "/foo") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(new OperationBuilder( "post-request-with-content", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST").content("Hello, world") - .build()); + .request("http://localhost/foo").method("POST").content(content).build()); } @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; + byte[] contentBytes = japaneseContent.getBytes("UTF-8"); this.snippet.expectHttpRequest("post-request-with-charset").withContents( httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "text/plain;charset=UTF-8") + .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); new HttpRequestSnippet().document(new OperationBuilder( "post-request-with-charset", this.snippet.getOutputDirectory()) .request("http://localhost/foo").method("POST") - .header("Content-Type", "text/plain;charset=UTF-8") - .content(japaneseContent.getBytes("UTF-8")).build()); + .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) + .build()); } @Test @@ -117,14 +120,15 @@ public void postRequestWithParameter() throws IOException { @Test public void putRequestWithContent() throws IOException { + String content = "Hello, world"; this.snippet.expectHttpRequest("put-request-with-content").withContents( - httpRequest(RequestMethod.PUT, "/foo").header(HttpHeaders.HOST, - "localhost").content("Hello, world")); + httpRequest(RequestMethod.PUT, "/foo") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(new OperationBuilder( "put-request-with-content", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").content("Hello, world") - .build()); + .request("http://localhost/foo").method("PUT").content(content).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index b2a259401..4959e7774 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -78,22 +78,27 @@ public void responseWithHeaders() throws IOException { @Test public void responseWithContent() throws IOException { + String content = "content"; this.snippet.expectHttpResponse("response-with-content").withContents( - httpResponse(HttpStatus.OK).content("content")); + httpResponse(HttpStatus.OK).content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpResponseSnippet().document(new OperationBuilder("response-with-content", - this.snippet.getOutputDirectory()).response().content("content").build()); + this.snippet.getOutputDirectory()).response().content(content).build()); } @Test public void responseWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; + byte[] contentBytes = japaneseContent.getBytes("UTF-8"); this.snippet.expectHttpResponse("response-with-charset").withContents( - httpResponse(HttpStatus.OK).header("Content-Type", - "text/plain;charset=UTF-8").content(japaneseContent)); + httpResponse(HttpStatus.OK) + .header("Content-Type", "text/plain;charset=UTF-8") + .content(japaneseContent) + .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", this.snippet.getOutputDirectory()).response() - .header("Content-Type", "text/plain;charset=UTF-8") - .content(japaneseContent.getBytes("UTF-8")).build()); + .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index 5a38b1bde..2f38bbb36 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -71,16 +71,6 @@ public void modifyResponseContent() { assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes()))); } - @Test - public void unknownContentLengthIsUnchanged() { - StandardOperationRequest request = new StandardOperationRequest( - URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), - new HttpHeaders(), new Parameters(), - Collections.emptyList()); - OperationRequest preprocessed = this.preprocessor.preprocess(request); - assertThat(preprocessed.getHeaders().getContentLength(), is(equalTo(-1L))); - } - @Test public void contentLengthIsUpdated() { HttpHeaders httpHeaders = new HttpHeaders(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index f0d54a812..d45388a09 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -164,6 +164,12 @@ public T header(String name, String value) { return (T) this; } + @SuppressWarnings("unchecked") + public T header(String name, long value) { + this.addLine(this.headerOffset++, name + ": " + value); + return (T) this; + } + } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java index 0fd6d7caa..536f831d4 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java @@ -29,7 +29,6 @@ import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; -import org.springframework.util.StringUtils; import org.springframework.web.context.WebApplicationContext; /** @@ -62,8 +61,7 @@ public class RestDocumentationMockMvcConfigurer extends MockMvcConfigurerAdapter RestDocumentationMockMvcConfigurer(RestDocumentation restDocumentation) { this.requestPostProcessor = new ConfigurerApplyingRequestPostProcessor( restDocumentation, this.uriConfigurer, this.writerResolverConfigurer, - this.snippetConfigurer, new ContentLengthHeaderConfigurer(), - this.templateEngineConfigurer); + this.snippetConfigurer, this.templateEngineConfigurer); } /** @@ -115,19 +113,6 @@ public RequestPostProcessor beforeMockMvcCreated( return this.requestPostProcessor; } - private static final class ContentLengthHeaderConfigurer extends AbstractConfigurer { - - @Override - void apply(MockHttpServletRequest request) { - long contentLength = request.getContentLengthLong(); - if (contentLength > 0 - && !StringUtils.hasText(request.getHeader("Content-Length"))) { - request.addHeader("Content-Length", request.getContentLengthLong()); - } - } - - } - private static final class TemplateEngineConfigurer extends AbstractConfigurer { private TemplateEngine templateEngine = new MustacheTemplateEngine( diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java index 856405f5b..6770d31bb 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java @@ -157,7 +157,8 @@ public void mockMultipartFileUpload() throws Exception { OperationRequestPart part = request.getParts().iterator().next(); assertThat(part.getName(), is(equalTo("file"))); assertThat(part.getSubmittedFileName(), is(nullValue())); - assertThat(part.getHeaders().isEmpty(), is(true)); + assertThat(part.getHeaders().size(), is(1)); + assertThat(part.getHeaders().getContentLength(), is(4L)); assertThat(part.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 10985bea9..ad6f34539 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -79,6 +79,7 @@ import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -120,6 +121,21 @@ public void basicSnippetGeneration() throws Exception { "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } + @Test + public void curlSnippetWithContent() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) + .andExpect(status().isOk()).andDo(document("curl-snippet-with-content")); + assertThat(new File( + "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl " + "'http://localhost:8080/' -i -X POST " + + "-H 'Accept: application/json' -d 'content'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -298,24 +314,27 @@ public void preprocessedResponse() throws Exception { removeHeaders("a"), replacePattern(pattern, "\"<>\"")))); + String original = "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," + + "\"href\":\"href\"}]}"; assertThat( new File("build/generated-snippets/original-response/http-response.adoc"), is(snippet().withContents( httpResponse(HttpStatus.OK) .header("a", "alpha") .header("Content-Type", "application/json") - .content( - "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," - + "\"href\":\"href\"}]}")))); + .header(HttpHeaders.CONTENT_LENGTH, + original.getBytes().length).content(original)))); + String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); assertThat( new File( "build/generated-snippets/preprocessed-response/http-response.adoc"), is(snippet().withContents( - httpResponse(HttpStatus.OK).header("Content-Type", - "application/json").content( - String.format("{%n \"a\" : \"<>\",%n \"links\" :" - + " [ {%n \"rel\" : \"rel\",%n \"href\" :" - + " \"...\"%n } ]%n}"))))); + httpResponse(HttpStatus.OK) + .header("Content-Type", "application/json") + .header(HttpHeaders.CONTENT_LENGTH, + prettyPrinted.getBytes().length) + .content(prettyPrinted)))); } @Test diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java index d8f78dd5e..486bfd8e6 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java @@ -27,7 +27,6 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertEquals; @@ -94,17 +93,6 @@ public void noContentLengthHeaderWhenRequestHasNotContent() { assertThat(this.request.getHeader("Content-Length"), is(nullValue())); } - @Test - public void contentLengthHeaderIsSetWhenRequestHasContent() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( - this.restDocumentation).beforeMockMvcCreated(null, null); - byte[] content = "Hello, world".getBytes(); - this.request.setContent(content); - postProcessor.postProcessRequest(this.request); - assertThat(this.request.getHeader("Content-Length"), - is(equalTo(Integer.toString(content.length)))); - } - private void assertUriConfiguration(String scheme, String host, int port) { assertEquals(scheme, this.request.getScheme()); assertEquals(host, this.request.getServerName()); From 535bea24f915e87892d213eaa3f0223dddef0392 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 28 Sep 2015 15:16:20 +0100 Subject: [PATCH 002/898] Improve handling of Host header so a preprocessor can remove it Previously, the Host header was treated specially in the HTTP request snippet. If no Host header was specified, one would always be added prior to producing the snippet. This ensured that the snippet was valid (an HTTP 1.1 request must include a Host header), but came at the cost of some confusion about why a preprocessor could not remove it. This commit updates the special treatment of the Host header so that it's now performed in a central location so that all of the snippets can benefit from a Host header being added if one isn't provided. The handling of the Content-Length header has also been reworked so that it's performed in the same location. The curl request snippet has been updated so that it doesn't include setting the Host header on the command line; it's unnecessary as curl will automatically include a Host header in the request. The documentation of the Host header's special treatment (made in 2fc0420) that noted that it was unaffected by preprocessing has been reverted. Closes gh-134 --- .../customizing-requests-and-responses.adoc | 4 - .../docs/asciidoc/documenting-your-api.adoc | 3 +- .../restdocs/curl/CurlRequestSnippet.java | 86 +++++++++++++---- .../restdocs/http/HttpRequestSnippet.java | 7 -- .../operation/AbstractOperationMessage.java | 13 +-- .../restdocs/operation/HttpHeadersHelper.java | 66 +++++++++++++ .../operation/OperationRequestFactory.java | 96 +++++++++++++++++++ .../OperationRequestPartFactory.java | 49 ++++++++++ .../operation/OperationResponseFactory.java | 85 ++++++++++++++++ .../operation/StandardOperationRequest.java | 4 +- .../StandardOperationRequestPart.java | 6 +- .../operation/StandardOperationResponse.java | 5 +- ...ContentModifyingOperationPreprocessor.java | 29 ++---- .../HeaderRemovingOperationPreprocessor.java | 17 ++-- .../http/HttpRequestSnippetTests.java | 15 ++- .../ContentTypeLinkExtractorTests.java | 11 ++- .../LinkExtractorsPayloadTests.java | 6 +- ...ntModifyingOperationPreprocessorTests.java | 14 ++- ...derRemovingOperationPreprocessorTests.java | 16 +++- .../restdocs/test/OperationBuilder.java | 13 +-- .../MockMvcOperationRequestFactory.java | 28 +++--- .../MockMvcOperationResponseFactory.java | 6 +- ...kMvcRestDocumentationIntegrationTests.java | 26 +++-- 23 files changed, 466 insertions(+), 139 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/HttpHeadersHelper.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index d88fb06d6..f88a58795 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -77,10 +77,6 @@ different replacement can also be specified if you wish. `removeHeaders` on `Preprocessors` removes any occurrences of the named headers from the request or response. -NOTE: For an HTTP 1.1 request to be valid it must contain a `Host` header. Therefore, -irrespective of any preprocessing, the default HTTP request snippet will always contain a -`Host` header. - [[customizing-requests-and-responses-preprocessors-replace-patterns]] diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index e0e477f34..7fd84cdf3 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -388,8 +388,7 @@ call that is being documented | `http-request.adoc` | Contains the HTTP request that is equivalent to the `MockMvc` call that is being - documented. HTTP 1.1 requires a `Host` header. If you do not provide one via the - `MockMvc` API the snippet will add one automatically. +documented | `http-response.adoc` | Contains the HTTP response that was returned diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 4d1ce4461..73829f249 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -18,10 +18,13 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -43,6 +46,15 @@ */ public class CurlRequestSnippet extends TemplatedSnippet { + private static final Set HEADER_FILTERS; + static { + Set headerFilters = new HashSet(); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); + headerFilters.add(new BasicAuthHeaderFilter()); + HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); + } + /** * Creates a new {@code CurlRequestSnippet} with no additional attributes. */ @@ -76,9 +88,9 @@ private String getOptions(Operation operation) { StringWriter command = new StringWriter(); PrintWriter printer = new PrintWriter(command); writeIncludeHeadersInOutputOption(printer); - HttpHeaders headers = writeUserOptionIfNecessary(operation.getRequest(), printer); + writeUserOptionIfNecessary(operation.getRequest(), printer); writeHttpMethodIfNecessary(operation.getRequest(), printer); - writeHeaders(headers, printer); + writeHeaders(operation.getRequest().getHeaders(), printer); writePartsIfNecessary(operation.getRequest(), printer); writeContent(operation.getRequest(), printer); @@ -89,22 +101,12 @@ private void writeIncludeHeadersInOutputOption(PrintWriter writer) { writer.print("-i"); } - private HttpHeaders writeUserOptionIfNecessary(OperationRequest request, - PrintWriter writer) { - HttpHeaders headers = new HttpHeaders(); - headers.putAll(request.getHeaders()); - String authorization = headers.getFirst(HttpHeaders.AUTHORIZATION); - if (isAuthorizationBasicHeader(authorization)) { - String credentials = new String(Base64Utils.decodeFromString(authorization - .substring(5).trim())); + private void writeUserOptionIfNecessary(OperationRequest request, PrintWriter writer) { + List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { + String credentials = BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); writer.print(String.format(" -u '%s'", credentials)); - headers.remove(HttpHeaders.AUTHORIZATION); } - return headers; - } - - private boolean isAuthorizationBasicHeader(String header) { - return header != null && header.startsWith("Basic"); } private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) { @@ -115,7 +117,7 @@ private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter wr private void writeHeaders(HttpHeaders headers, PrintWriter writer) { for (Entry> entry : headers.entrySet()) { - if (!HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(entry.getKey())) { + if (allowedHeader(entry)) { for (String header : entry.getValue()) { writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } @@ -123,6 +125,15 @@ private void writeHeaders(HttpHeaders headers, PrintWriter writer) { } } + private boolean allowedHeader(Entry> header) { + for (HeaderFilter headerFilter : HEADER_FILTERS) { + if (!headerFilter.allow(header.getKey(), header.getValue())) { + return false; + } + } + return true; + } + private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { for (OperationRequestPart part : request.getParts()) { writer.printf(" -F '%s=", part.getName()); @@ -166,4 +177,45 @@ private boolean isPutOrPost(OperationRequest request) { || HttpMethod.POST.equals(request.getMethod()); } + private interface HeaderFilter { + + boolean allow(String name, List value); + } + + private static final class BasicAuthHeaderFilter implements HeaderFilter { + + @Override + public boolean allow(String name, List value) { + if (HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)) { + return false; + } + return true; + } + + static boolean isBasicAuthHeader(List value) { + return value != null && (!value.isEmpty()) + && value.get(0).startsWith("Basic "); + } + + static String decodeBasicAuthHeader(List value) { + return new String(Base64Utils.decodeFromString(value.get(0).substring(6))); + } + + } + + private static final class NamedHeaderFilter implements HeaderFilter { + + private final String name; + + private NamedHeaderFilter(String name) { + this.name = name; + } + + @Override + public boolean allow(String name, List value) { + return !this.name.equalsIgnoreCase(name); + } + + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 9b73fde48..9060ada91 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -79,9 +79,6 @@ protected Map createModel(Operation operation) { private List> getHeaders(OperationRequest request) { List> headers = new ArrayList<>(); - if (requiresHostHeader(request)) { - headers.add(header(HttpHeaders.HOST, request.getUri().getHost())); - } for (Entry> header : request.getHeaders().entrySet()) { for (String value : header.getValue()) { @@ -170,10 +167,6 @@ private void writeMultipartEnd(PrintWriter writer) { writer.printf("--%s--", MULTIPART_BOUNDARY); } - private boolean requiresHostHeader(OperationRequest request) { - return request.getHeaders().get(HttpHeaders.HOST) == null; - } - private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) { return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null && isPutOrPost(request) && !request.getParameters().isEmpty(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java index d111f321b..c5ede617d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -35,18 +35,7 @@ abstract class AbstractOperationMessage { AbstractOperationMessage(byte[] content, HttpHeaders headers) { this.content = content == null ? new byte[0] : content; - this.headers = createHeaders(content, headers); - } - - private static HttpHeaders createHeaders(byte[] content, HttpHeaders input) { - HttpHeaders headers = new HttpHeaders(); - if (input != null) { - headers.putAll(input); - } - if (content != null && content.length > 0 && headers.getContentLength() == -1) { - headers.setContentLength(content.length); - } - return headers; + this.headers = headers; } public byte[] getContent() { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/HttpHeadersHelper.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/HttpHeadersHelper.java new file mode 100644 index 000000000..a9cb7c5a0 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/HttpHeadersHelper.java @@ -0,0 +1,66 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; + +/** + * Helper for working with {@link HttpHeaders}. + * + * @author Andy Wilkinson + */ +class HttpHeadersHelper { + + private final HttpHeaders httpHeaders; + + HttpHeadersHelper(HttpHeaders httpHeaders) { + HttpHeaders headers = new HttpHeaders(); + if (httpHeaders != null) { + headers.putAll(httpHeaders); + } + this.httpHeaders = headers; + } + + HttpHeadersHelper addIfAbsent(String name, String value) { + if (this.httpHeaders.get(name) == null) { + this.httpHeaders.add(name, value); + } + return this; + } + + HttpHeadersHelper updateContentLengthHeaderIfPresent(byte[] content) { + if (this.httpHeaders.getContentLength() != -1) { + setContentLengthHeader(content); + } + return this; + } + + HttpHeadersHelper setContentLengthHeader(byte[] content) { + if (content == null || content.length == 0) { + this.httpHeaders.remove(HttpHeaders.CONTENT_LENGTH); + } + else { + this.httpHeaders.setContentLength(content.length); + } + return this; + } + + HttpHeaders getHeaders() { + return HttpHeaders.readOnlyHttpHeaders(this.httpHeaders); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java new file mode 100644 index 000000000..693be1ddb --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -0,0 +1,96 @@ +/* + * 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.operation; + +import java.net.URI; +import java.util.Collection; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +/** + * A factory for creating {@link OperationRequest OperationRequests}. + * + * @author Andy Wilkinson + */ +public class OperationRequestFactory { + + /** + * Creates a new {@link OperationRequest}. The given {@code headers} will be augmented + * to ensure that they always include a {@code Content-Length} header if the request + * has any content and a {@code Host} header. + * + * @param uri the request's uri + * @param method the request method + * @param content the content of the request + * @param headers the request's headers + * @param parameters the request's parameters + * @param parts the request's parts + * @return the {@code OperationRequest} + */ + public OperationRequest create(URI uri, HttpMethod method, byte[] content, + HttpHeaders headers, Parameters parameters, + Collection parts) { + return new StandardOperationRequest(uri, method, content, augmentHeaders(headers, + uri, content), parameters, parts); + } + + /** + * Creates a new {@code OperationRequest} based on the given {@code original} but with + * the given {@code newContent}. If the original request had a {@code Content-Length} + * header it will be modified to match the length of the new content. + * + * @param original The original request + * @param newContent The new content + * + * @return The new request with the new content + */ + public OperationRequest createFrom(OperationRequest original, byte[] newContent) { + return new StandardOperationRequest(original.getUri(), original.getMethod(), + newContent, getUpdatedHeaders(original.getHeaders(), newContent), + original.getParameters(), original.getParts()); + } + + /** + * Creates a new {@code OperationRequest} based on the given {@code original} but with + * the given {@code newHeaders}. + * + * @param original The original request + * @param newHeaders The new headers + * + * @return The new request with the new content + */ + public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { + return new StandardOperationRequest(original.getUri(), original.getMethod(), + original.getContent(), newHeaders, original.getParameters(), + original.getParts()); + } + + private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, + byte[] content) { + return new HttpHeadersHelper(originalHeaders) + .addIfAbsent(HttpHeaders.HOST, uri.getHost()) + .setContentLengthHeader(content).getHeaders(); + } + + private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, + byte[] updatedContent) { + return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent( + updatedContent).getHeaders(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java new file mode 100644 index 000000000..47aa7e431 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestPartFactory.java @@ -0,0 +1,49 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; + +/** + * A factory for creating {@link OperationRequestPart OperationRequestParts}. + * + * @author Andy Wilkinson + */ +public class OperationRequestPartFactory { + + /** + * Creates a new {@link OperationRequestPart}. The given {@code headers} will be + * augmented to ensure that they always include a {@code Content-Length} header if the + * part has any content. + * + * @param name the name of the part + * @param submittedFileName the name of the file being submitted by the part + * @param content the content of the part + * @param headers the headers of the part + * @return the {@code OperationRequestPart} + */ + public OperationRequestPart create(String name, String submittedFileName, + byte[] content, HttpHeaders headers) { + return new StandardOperationRequestPart(name, submittedFileName, content, + augmentHeaders(headers, content)); + } + + private HttpHeaders augmentHeaders(HttpHeaders input, byte[] content) { + return new HttpHeadersHelper(input).setContentLengthHeader(content).getHeaders(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java new file mode 100644 index 000000000..c3417c9f8 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -0,0 +1,85 @@ +/* + * 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.operation; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * A factory for creating {@link OperationResponse OperationResponses}. + * + * @author Andy Wilkinson + */ +public class OperationResponseFactory { + + /** + * Creates a new {@link OperationResponse}. If the response has any content, the given + * {@code headers} will be augmented to ensure that they include a + * {@code Content-Length} header. + * + * @param status the status of the response + * @param headers the request's headers + * @param content the content of the request + * @return the {@code OperationResponse} + */ + public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] content) { + return new StandardOperationResponse(status, augmentHeaders(headers, content), + content); + } + + /** + * Creates a new {@code OperationResponse} based on the given {@code original} but + * with the given {@code newContent}. If the original response had a + * {@code Content-Length} header it will be modified to match the length of the new + * content. + * + * @param original The original response + * @param newContent The new content + * + * @return The new response with the new content + */ + public OperationResponse createFrom(OperationResponse original, byte[] newContent) { + return new StandardOperationResponse(original.getStatus(), getUpdatedHeaders( + original.getHeaders(), newContent), newContent); + } + + /** + * Creates a new {@code OperationResponse} based on the given {@code original} but + * with the given {@code newHeaders}. + * + * @param original The original response + * @param newHeaders The new headers + * + * @return The new response with the new headers + */ + public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) { + return new StandardOperationResponse(original.getStatus(), newHeaders, + original.getContent()); + } + + private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, byte[] content) { + return new HttpHeadersHelper(originalHeaders).setContentLengthHeader(content) + .getHeaders(); + } + + private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, + byte[] updatedContent) { + return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent( + updatedContent).getHeaders(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index c869100b4..305d7b2a7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -28,7 +28,7 @@ * * @author Andy Wilkinson */ -public class StandardOperationRequest extends AbstractOperationMessage implements +class StandardOperationRequest extends AbstractOperationMessage implements OperationRequest { private HttpMethod method; @@ -50,7 +50,7 @@ public class StandardOperationRequest extends AbstractOperationMessage implement * @param parameters the parameters * @param parts the parts */ - public StandardOperationRequest(URI uri, HttpMethod method, byte[] content, + StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, Collection parts) { super(content, headers); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java index c0f6e8917..8ea231a4d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java @@ -23,7 +23,7 @@ * * @author Andy Wilkinson */ -public class StandardOperationRequestPart extends AbstractOperationMessage implements +class StandardOperationRequestPart extends AbstractOperationMessage implements OperationRequestPart { private final String name; @@ -38,8 +38,8 @@ public class StandardOperationRequestPart extends AbstractOperationMessage imple * @param content the contents of the part * @param headers the headers of the part */ - public StandardOperationRequestPart(String name, String submittedFileName, - byte[] content, HttpHeaders headers) { + StandardOperationRequestPart(String name, String submittedFileName, byte[] content, + HttpHeaders headers) { super(content, headers); this.name = name; this.submittedFileName = submittedFileName; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index 1e42beb20..32dfa57f7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -24,7 +24,7 @@ * * @author Andy Wilkinson */ -public class StandardOperationResponse extends AbstractOperationMessage implements +class StandardOperationResponse extends AbstractOperationMessage implements OperationResponse { private final HttpStatus status; @@ -37,8 +37,7 @@ public class StandardOperationResponse extends AbstractOperationMessage implemen * @param headers the headers of the response * @param content the content of the response */ - public StandardOperationResponse(HttpStatus status, HttpHeaders headers, - byte[] content) { + StandardOperationResponse(HttpStatus status, HttpHeaders headers, byte[] content) { super(content, headers); this.status = status; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index 5cc31561b..60c704aa6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -16,11 +16,10 @@ package org.springframework.restdocs.operation.preprocess; -import org.springframework.http.HttpHeaders; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; /** * An {@link OperationPreprocessor} that applies a {@link ContentModifier} to the content @@ -30,6 +29,10 @@ */ public class ContentModifyingOperationPreprocessor implements OperationPreprocessor { + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final ContentModifier contentModifier; /** @@ -46,30 +49,14 @@ public ContentModifyingOperationPreprocessor(ContentModifier contentModifier) { public OperationRequest preprocess(OperationRequest request) { byte[] modifiedContent = this.contentModifier.modifyContent(request.getContent(), request.getHeaders().getContentType()); - return new StandardOperationRequest(request.getUri(), request.getMethod(), - modifiedContent, - getUpdatedHeaders(request.getHeaders(), modifiedContent), - request.getParameters(), request.getParts()); + return this.requestFactory.createFrom(request, modifiedContent); } @Override public OperationResponse preprocess(OperationResponse response) { byte[] modifiedContent = this.contentModifier.modifyContent( response.getContent(), response.getHeaders().getContentType()); - return new StandardOperationResponse(response.getStatus(), getUpdatedHeaders( - response.getHeaders(), modifiedContent), modifiedContent); - } - - private HttpHeaders getUpdatedHeaders(HttpHeaders headers, byte[] updatedContent) { - HttpHeaders updatedHeaders = new HttpHeaders(); - updatedHeaders.putAll(headers); - if (updatedContent.length > 0) { - updatedHeaders.setContentLength(updatedContent.length); - } - else { - updatedHeaders.remove(HttpHeaders.CONTENT_LENGTH); - } - return updatedHeaders; + return this.responseFactory.createFrom(response, modifiedContent); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java index 36eba207c..7766af57c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java @@ -22,9 +22,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; /** * An {@link OperationPreprocessor} that removes headers. @@ -33,6 +33,10 @@ */ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final Set headersToRemove; HeaderRemovingOperationPreprocessor(String... headersToRemove) { @@ -41,15 +45,14 @@ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { @Override public OperationResponse preprocess(OperationResponse response) { - return new StandardOperationResponse(response.getStatus(), - removeHeaders(response.getHeaders()), response.getContent()); + return this.responseFactory.createFrom(response, + removeHeaders(response.getHeaders())); } @Override public OperationRequest preprocess(OperationRequest request) { - return new StandardOperationRequest(request.getUri(), request.getMethod(), - request.getContent(), removeHeaders(request.getHeaders()), - request.getParameters(), request.getParts()); + return this.requestFactory.createFrom(request, + removeHeaders(request.getHeaders())); } private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 9ccb89f1d..f948fb103 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -54,8 +54,8 @@ public class HttpRequestSnippetTests { @Test public void getRequest() throws IOException { this.snippet.expectHttpRequest("get-request").withContents( - httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, - "localhost").header("Alpha", "a")); + httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a").header( + HttpHeaders.HOST, "localhost")); new HttpRequestSnippet().document(new OperationBuilder("get-request", this.snippet.getOutputDirectory()).request("http://localhost/foo") @@ -92,8 +92,8 @@ public void postRequestWithCharset() throws IOException { byte[] contentBytes = japaneseContent.getBytes("UTF-8"); this.snippet.expectHttpRequest("post-request-with-charset").withContents( httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "text/plain;charset=UTF-8") + .header(HttpHeaders.HOST, "localhost") .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); @@ -151,10 +151,9 @@ public void multipartPost() throws IOException { + "form-data; " + "name=image%n%n<< data >>")); this.snippet.expectHttpRequest("multipart-post").withContents( httpRequest(RequestMethod.POST, "/upload") - .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) - .content(expectedContent)); + .header(HttpHeaders.HOST, "localhost").content(expectedContent)); new HttpRequestSnippet().document(new OperationBuilder("multipart-post", this.snippet.getOutputDirectory()).request("http://localhost/upload") .method("POST") @@ -175,10 +174,9 @@ public void multipartPostWithParameters() throws IOException { String expectedContent = param1Part + param2Part + param3Part + filePart; this.snippet.expectHttpRequest("multipart-post-with-parameters").withContents( httpRequest(RequestMethod.POST, "/upload") - .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) - .content(expectedContent)); + .header(HttpHeaders.HOST, "localhost").content(expectedContent)); new HttpRequestSnippet().document(new OperationBuilder( "multipart-post-with-parameters", this.snippet.getOutputDirectory()) .request("http://localhost/upload").method("POST") @@ -194,10 +192,9 @@ public void multipartPostWithContentType() throws IOException { + "image/png%n%n<< data >>")); this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents( httpRequest(RequestMethod.POST, "/upload") - .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) - .content(expectedContent)); + .header(HttpHeaders.HOST, "localhost").content(expectedContent)); new HttpRequestSnippet().document(new OperationBuilder( "multipart-post-with-content-type", this.snippet.getOutputDirectory()) .request("http://localhost/upload").method("POST") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index 5157e07bb..4ff3dbb4e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -26,7 +26,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -38,13 +39,15 @@ */ public class ContentTypeLinkExtractorTests { + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + @Rule public ExpectedException thrown = ExpectedException.none(); @Test public void extractionFailsWithNullContentType() throws IOException { this.thrown.expect(IllegalStateException.class); - new ContentTypeLinkExtractor().extractLinks(new StandardOperationResponse( + new ContentTypeLinkExtractor().extractLinks(this.responseFactory.create( HttpStatus.OK, new HttpHeaders(), null)); } @@ -55,7 +58,7 @@ public void extractorCalledWithMatchingContextType() throws IOException { extractors.put(MediaType.APPLICATION_JSON, extractor); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); - StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, null); new ContentTypeLinkExtractor(extractors).extractLinks(response); verify(extractor).extractLinks(response); @@ -68,7 +71,7 @@ public void extractorCalledWithCompatibleContextType() throws IOException { extractors.put(MediaType.APPLICATION_JSON, extractor); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.parseMediaType("application/json;foo=bar")); - StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + OperationResponse response = this.responseFactory.create(HttpStatus.OK, httpHeaders, null); new ContentTypeLinkExtractor(extractors).extractLinks(response); verify(extractor).extractLinks(response); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 02adf5cbb..47a05424a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -30,7 +30,7 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.util.FileCopyUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -46,6 +46,8 @@ @RunWith(Parameterized.class) public class LinkExtractorsPayloadTests { + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final LinkExtractor linkExtractor; private final String linkType; @@ -107,7 +109,7 @@ private void assertLinks(List expectedLinks, Map> actua } private OperationResponse createResponse(String contentName) throws IOException { - return new StandardOperationResponse(HttpStatus.OK, null, + return this.responseFactory.create(HttpStatus.OK, null, FileCopyUtils.copyToByteArray(getPayloadFile(contentName))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index 2f38bbb36..330381f68 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -25,11 +25,11 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationResponse; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -43,6 +43,10 @@ */ public class ContentModifyingOperationPreprocessorTests { + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final ContentModifyingOperationPreprocessor preprocessor = new ContentModifyingOperationPreprocessor( new ContentModifier() { @@ -55,7 +59,7 @@ public byte[] modifyContent(byte[] originalContent, MediaType mediaType) { @Test public void modifyRequestContent() { - StandardOperationRequest request = new StandardOperationRequest( + OperationRequest request = this.requestFactory.create( URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), new HttpHeaders(), new Parameters(), Collections.emptyList()); @@ -65,7 +69,7 @@ public void modifyRequestContent() { @Test public void modifyResponseContent() { - StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + OperationResponse response = this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), "content".getBytes()); OperationResponse preprocessed = this.preprocessor.preprocess(response); assertThat(preprocessed.getContent(), is(equalTo("modified".getBytes()))); @@ -75,7 +79,7 @@ public void modifyResponseContent() { public void contentLengthIsUpdated() { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentLength(7); - StandardOperationRequest request = new StandardOperationRequest( + OperationRequest request = this.requestFactory.create( URI.create("http://localhost"), HttpMethod.GET, "content".getBytes(), httpHeaders, new Parameters(), Collections.emptyList()); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index 808b9ab91..634ef6162 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -25,11 +25,11 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationResponse; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -44,23 +44,29 @@ */ public class HeaderRemovingOperationPreprocessorTests { + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor( "b"); @Test public void modifyRequestHeaders() { - StandardOperationRequest request = new StandardOperationRequest( + OperationRequest request = this.requestFactory.create( URI.create("http://localhost"), HttpMethod.GET, new byte[0], getHttpHeaders(), new Parameters(), Collections.emptyList()); OperationRequest preprocessed = this.preprocessor.preprocess(request); - assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); + assertThat(preprocessed.getHeaders().size(), is(equalTo(2))); assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); + assertThat(preprocessed.getHeaders(), + hasEntry("Host", Arrays.asList("localhost"))); } @Test public void modifyResponseHeaders() { - StandardOperationResponse response = new StandardOperationResponse(HttpStatus.OK, + OperationResponse response = this.responseFactory.create(HttpStatus.OK, getHttpHeaders(), new byte[0]); OperationResponse preprocessed = this.preprocessor.preprocess(response); assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 51d66504a..e5be81665 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -29,13 +29,13 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.StandardOperation; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationRequestPart; -import org.springframework.restdocs.operation.StandardOperationResponse; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; @@ -122,7 +122,7 @@ private OperationRequest buildRequest() { for (OperationRequestPartBuilder builder : this.partBuilders) { parts.add(builder.buildPart()); } - return new StandardOperationRequest(this.requestUri, this.method, + return new OperationRequestFactory().create(this.requestUri, this.method, this.content, this.headers, this.parameters, parts); } @@ -196,7 +196,7 @@ public Operation build() { } private OperationRequestPart buildPart() { - return new StandardOperationRequestPart(this.name, + return new OperationRequestPartFactory().create(this.name, this.submittedFileName, this.content, this.headers); } @@ -219,7 +219,8 @@ public final class OperationResponseBuilder { private byte[] content = new byte[0]; private OperationResponse buildResponse() { - return new StandardOperationResponse(this.status, this.headers, this.content); + return new OperationResponseFactory().create(this.status, this.headers, + this.content); } public OperationResponseBuilder status(int status) { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java index 417f29e82..9449354a8 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java @@ -33,10 +33,10 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartHttpServletRequest; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.operation.StandardOperationRequest; -import org.springframework.restdocs.operation.StandardOperationRequestPart; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; @@ -67,7 +67,7 @@ class MockMvcOperationRequestFactory { * @return the {@code OperationRequest} * @throws Exception if the request could not be created */ - public OperationRequest createOperationRequest(MockHttpServletRequest mockRequest) + OperationRequest createOperationRequest(MockHttpServletRequest mockRequest) throws Exception { HttpHeaders headers = extractHeaders(mockRequest); Parameters parameters = extractParameters(mockRequest); @@ -76,8 +76,9 @@ public OperationRequest createOperationRequest(MockHttpServletRequest mockReques if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { queryString = parameters.toQueryString(); } - return new StandardOperationRequest(URI.create(getRequestUri(mockRequest) - + (StringUtils.hasText(queryString) ? "?" + queryString : "")), + return new OperationRequestFactory().create( + URI.create(getRequestUri(mockRequest) + + (StringUtils.hasText(queryString) ? "?" + queryString : "")), HttpMethod.valueOf(mockRequest.getMethod()), FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers, parameters, parts); @@ -102,16 +103,17 @@ private List extractServletRequestParts( return parts; } - private StandardOperationRequestPart createOperationRequestPart(Part part) - throws IOException { + private OperationRequestPart createOperationRequestPart(Part part) throws IOException { HttpHeaders partHeaders = extractHeaders(part); List contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE); if (part.getContentType() != null && contentTypeHeader == null) { partHeaders.setContentType(MediaType.parseMediaType(part.getContentType())); } - return new StandardOperationRequestPart(part.getName(), StringUtils.hasText(part - .getSubmittedFileName()) ? part.getSubmittedFileName() : null, - FileCopyUtils.copyToByteArray(part.getInputStream()), partHeaders); + return new OperationRequestPartFactory() + .create(part.getName(), + StringUtils.hasText(part.getSubmittedFileName()) ? part + .getSubmittedFileName() : null, FileCopyUtils + .copyToByteArray(part.getInputStream()), partHeaders); } private List extractMultipartRequestParts( @@ -126,14 +128,14 @@ private List extractMultipartRequestParts( return parts; } - private StandardOperationRequestPart createOperationRequestPart(MultipartFile file) + private OperationRequestPart createOperationRequestPart(MultipartFile file) throws IOException { HttpHeaders partHeaders = new HttpHeaders(); if (StringUtils.hasText(file.getContentType())) { partHeaders.setContentType(MediaType.parseMediaType(file.getContentType())); } - return new StandardOperationRequestPart(file.getName(), StringUtils.hasText(file - .getOriginalFilename()) ? file.getOriginalFilename() : null, + return new OperationRequestPartFactory().create(file.getName(), StringUtils + .hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : null, file.getBytes(), partHeaders); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java index 549156976..4e4f765f5 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java @@ -20,7 +20,7 @@ import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; /** * A factory for creating an {@link OperationResponse} derived from a @@ -36,8 +36,8 @@ class MockMvcOperationResponseFactory { * @param mockResponse the response * @return the {@code OperationResponse} */ - public OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) { - return new StandardOperationResponse( + OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) { + return new OperationResponseFactory().create( HttpStatus.valueOf(mockResponse.getStatus()), extractHeaders(mockResponse), mockResponse.getContentAsByteArray()); } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index ad6f34539..a7422f943 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -269,33 +269,31 @@ public void preprocessedRequest() throws Exception { .andDo(document("original-request")) .andDo(document( "preprocessed-request", - preprocessRequest(prettyPrint(), removeHeaders("a"), + preprocessRequest( + prettyPrint(), + removeHeaders("a", HttpHeaders.HOST, + HttpHeaders.CONTENT_LENGTH), replacePattern(pattern, "\"<>\"")))); assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("Host", "localhost") - .header("a", "alpha").header("b", "bravo") + httpRequest(RequestMethod.GET, "/").header("a", "alpha") + .header("b", "bravo") .header("Content-Type", "application/json") .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Host", "localhost") .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet() - .withContents( - httpRequest(RequestMethod.GET, "/") - .header("Host", "localhost") - .header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", - MediaType.APPLICATION_JSON_VALUE) - .header("Content-Length", - Integer.toString(prettyPrinted.getBytes().length)) - .content(prettyPrinted)))); + is(snippet().withContents( + httpRequest(RequestMethod.GET, "/").header("b", "bravo") + .header("Content-Type", "application/json") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .content(prettyPrinted)))); } @Test From 0d2c24ccc16ff6f2e69356dfa1639965c9633b57 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 28 Sep 2015 16:18:27 +0100 Subject: [PATCH 003/898] Improve curl request snippet's handling of query parameters Previously, if a request had no content and it was a POST or a PUT request, the curl request snippet would use the request's parameters as application/x-www-form-urlencoded data sent using the -d option. This resulted in the wrong snippet being produced if those parameters had actually come from the request's query string. This commit enhances CurlRequestSnippet so that, when it's using the request's parameter's to create its content, it only includes parameters that are not specified in the query string. With these changes in place, this MockMvc request: post("/?foo=bar").param("foo", "bar").param("a", "alpha") Will produce a curl snippet with the following command: $ curl 'http://localhost:8080/?foo=bar' -i -X POST -d 'a=alpha' foo=bar only appears in the command's query string, despite also being a general request parameter, and a=alpha only appears in the request's data. Closes gh-139 --- .../restdocs/curl/CurlRequestSnippet.java | 39 +++++++- .../restdocs/curl/QueryStringParser.java | 83 +++++++++++++++++ .../curl/CurlRequestSnippetTests.java | 41 ++++++++- .../restdocs/curl/QueryStringParserTests.java | 92 +++++++++++++++++++ ...kMvcRestDocumentationIntegrationTests.java | 19 ++++ 5 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 73829f249..56f1fd4ca 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -31,6 +31,7 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.Base64Utils; @@ -165,9 +166,41 @@ else if (!request.getParts().isEmpty()) { } } else if (isPutOrPost(request)) { - String queryString = request.getParameters().toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.print(String.format(" -d '%s'", queryString)); + writeContentUsingParameters(request, writer); + } + } + + private void writeContentUsingParameters(OperationRequest request, PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + String queryString = uniqueParameters.toQueryString(); + if (StringUtils.hasText(queryString)) { + writer.print(String.format(" -d '%s'", queryString)); + } + } + + private Parameters getUniqueParameters(OperationRequest request) { + Parameters queryStringParameters = new QueryStringParser() + .parse(request.getUri()); + Parameters uniqueParameters = new Parameters(); + + for (Entry> parameter : request.getParameters().entrySet()) { + addIfUnique(parameter, queryStringParameters, uniqueParameters); + } + return uniqueParameters; + } + + private void addIfUnique(Entry> parameter, + Parameters queryStringParameters, Parameters uniqueParameters) { + if (!queryStringParameters.containsKey(parameter.getKey())) { + uniqueParameters.put(parameter.getKey(), parameter.getValue()); + } + else { + List candidates = parameter.getValue(); + List existing = queryStringParameters.get(parameter.getKey()); + for (String candidate : candidates) { + if (!existing.contains(candidate)) { + uniqueParameters.add(parameter.getKey(), candidate); + } } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java new file mode 100644 index 000000000..d09d6e441 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java @@ -0,0 +1,83 @@ +/* + * 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.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Scanner; + +import org.springframework.restdocs.operation.Parameters; + +/** + * A parser for the query string of a URI. + * + * @author Andy Wilkinson + */ +public class QueryStringParser { + + /** + * Parses the query string of the given {@code uri} and returns the resulting + * {@link Parameters}. + * + * @param uri the uri to parse + * @return the parameters parsed from the query string + */ + public Parameters parse(URI uri) { + String query = uri.getRawQuery(); + if (query != null) { + return parse(query); + } + return new Parameters(); + } + + private Parameters parse(String query) { + Parameters parameters = new Parameters(); + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private void processParameter(String parameter, Parameters parameters) { + String[] components = parameter.split("="); + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + throw new IllegalArgumentException("The parameter '" + parameter + + "' is malformed"); + } + } + + private String decode(String encoded) { + try { + return URLDecoder.decode(encoded, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Unable to URL encode " + encoded + + " using UTF-8", ex); + } + + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index f84cf0e18..21c1973d6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -84,7 +84,7 @@ public void requestWithContent() throws IOException { } @Test - public void requestWithQueryString() throws IOException { + public void getRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("request-with-query-string") .withContents( codeBlock("bash").content( @@ -94,6 +94,16 @@ public void requestWithQueryString() throws IOException { "http://localhost/foo?param=value").build()); } + @Test + public void postRequestWithQueryString() throws IOException { + this.snippet.expectCurlRequest("post-request-with-query-string").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?param=value' -i -X POST")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-query-string", this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param=value").method("POST").build()); + } + @Test public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( @@ -132,6 +142,35 @@ public void postRequestWithUrlEncodedParameter() throws IOException { .method("POST").param("k1", "a&b").build()); } + @Test + public void postRequestWithQueryStringAndParameter() throws IOException { + this.snippet + .expectCurlRequest("post-request-with-query-string-and-parameter") + .withContents( + codeBlock("bash") + .content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-query-string-and-parameter", this.snippet + .getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("b", "bravo").build()); + } + + @Test + public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectCurlRequest( + "post-request-with-overlapping-query-string-and-parameters") + .withContents( + codeBlock("bash") + .content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-overlapping-query-string-and-parameters", this.snippet + .getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); + } + @Test public void putRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java new file mode 100644 index 000000000..bfdc70107 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-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.net.URI; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.operation.Parameters; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link QueryStringParser}. + * + * @author Andy Wilkinson + */ +public class QueryStringParserTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final QueryStringParser queryStringParser = new QueryStringParser(); + + @Test + public void noParameters() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost")); + assertThat(parameters.size(), is(equalTo(0))); + } + + @Test + public void singleParameter() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=alpha")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); + } + + @Test + public void multipleParameters() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=alpha&b=bravo&c=charlie")); + assertThat(parameters.size(), is(equalTo(3))); + assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); + assertThat(parameters, hasEntry("b", Arrays.asList("bravo"))); + assertThat(parameters, hasEntry("c", Arrays.asList("charlie"))); + } + + @Test + public void multipleParametersWithSameKey() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=apple&a=avocado")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("apple", "avocado"))); + } + + @Test + public void encoded() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=al%26%3Dpha")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("al&=pha"))); + } + + @Test + public void malformedParameter() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown + .expectMessage(equalTo("The parameter 'a=apple=avocado' is malformed")); + this.queryStringParser.parse(URI.create("http://localhost?a=apple=avocado")); + } +} diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index a7422f943..cbf725447 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -136,6 +136,25 @@ public void curlSnippetWithContent() throws Exception { + "-H 'Accept: application/json' -d 'content'")))); } + @Test + public void curlSnippetWithQueryStringOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform( + post("/?foo=bar").param("foo", "bar").param("a", "alpha") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("curl-snippet-with-query-string")); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl " + + "'http://localhost:8080/?foo=bar' -i -X POST " + + "-H 'Accept: application/json' -d 'a=alpha'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) From 5a010160a15d2907647358d76b34b46a62a88c78 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 28 Sep 2015 20:11:11 +0100 Subject: [PATCH 004/898] Polish Snippet-related javadoc Closes gh-136 --- .../restdocs/curl/CurlDocumentation.java | 16 +-- .../restdocs/http/HttpDocumentation.java | 27 ++--- .../hypermedia/HypermediaDocumentation.java | 99 +++++++++++++------ .../payload/PayloadDocumentation.java | 81 +++++++-------- .../request/RequestDocumentation.java | 73 ++++++++------ .../mockmvc/MockMvcRestDocumentation.java | 24 ++--- .../RestDocumentationMockMvcConfigurer.java | 18 ++++ 7 files changed, 209 insertions(+), 129 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index be692cc93..a3549443b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -36,22 +36,22 @@ private CurlDocumentation() { } /** - * Returns a handler that will produce a snippet containing the curl request for the - * API call. + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. * - * @return the handler that will produce the snippet + * @return the snippet that will document the curl request */ 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. + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. * - * @param attributes Attributes made available during rendering of the curl request - * snippet - * @return the handler that will produce the snippet + * @param attributes the attributes + * @return the snippet that will document the curl request */ public static Snippet curlRequest(Map attributes) { return new CurlRequestSnippet(attributes); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 9aae38b71..f1bc8b389 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -33,41 +33,44 @@ private HttpDocumentation() { } /** - * Returns a handler that will produce a snippet containing the HTTP request for the - * API call. + * Returns a new {@code Snippet} that will document the HTTP request for the API + * operation. * - * @return the handler that will produce the snippet + * @return the snippet that will document the HTTP request */ public static Snippet httpRequest() { return new HttpRequestSnippet(); } /** - * 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. + * Returns a new {@code Snippet} that will document the HTTP request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. * * @param attributes the attributes - * @return the handler that will produce the snippet + * @return the snippet that will document the HTTP request */ public static Snippet httpRequest(Map attributes) { return new HttpRequestSnippet(attributes); } /** - * Returns a handler that will produce a snippet containing the HTTP response for the - * API call. - * @return the handler that will produce the snippet + * Returns a {@code Snippet} that will document the HTTP response for the API + * operation. + * + * @return the snippet that will document the HTTP response */ public static Snippet httpResponse() { return new HttpResponseSnippet(); } /** - * 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. + * Returns a {@code Snippet} that will document the HTTP response for the API + * operation. The given {@code attributes} will be available during snippet + * generation. * * @param attributes the attributes - * @return the handler that will produce the snippet + * @return the snippet that will document the HTTP response */ public static Snippet httpResponse(Map attributes) { return new HttpResponseSnippet(attributes); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index b167225e7..c073484e3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -43,12 +43,17 @@ public static LinkDescriptor linkWithRel(String rel) { } /** - * 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. + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response automatically based on its + * content type and will be documented using the given {@code descriptors}. + *

+ * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. * - * @param descriptors The descriptions of the response's links - * @return the handler + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links */ public static Snippet links(LinkDescriptor... descriptors) { return new LinksSnippet(new ContentTypeLinkExtractor(), @@ -56,14 +61,19 @@ public static Snippet links(LinkDescriptor... 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. + * Returns a new {@code Snippet} that will document 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 + * and will be documented using the given {@code descriptors}. + *

+ * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. * - * @param attributes Attributes made available during rendering of the links snippet - * @param descriptors The descriptions of the response's links - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links */ public static Snippet links(Map attributes, LinkDescriptor... descriptors) { @@ -72,13 +82,18 @@ public static Snippet links(Map attributes, } /** - * 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}. + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response using the given + * {@code linkExtractor} and will be documented using the given {@code descriptors}. + *

+ * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. * - * @param linkExtractor Used to extract the links from the response - * @param descriptors The descriptions of the response's links - * @return the handler + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links */ public static Snippet links(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { @@ -86,15 +101,20 @@ public static Snippet links(LinkExtractor linkExtractor, } /** - * 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}. + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response using the given {@code linkExtractor} and + * will be documented using the given {@code descriptors}. + *

+ * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. * - * @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 + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links */ public static Snippet links(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { @@ -104,9 +124,19 @@ public static Snippet links(LinkExtractor linkExtractor, /** * 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}. + * {@code _links}. For example: * - * @return The extract for HAL-style links + *

+	 * {
+	 *     "_links": {
+	 *         "self": {
+	 *             "href": "http://example.com/foo"
+	 *         }
+	 *     }
+	 * }
+	 * 
+ * + * @return The extractor for HAL-style links */ public static LinkExtractor halLinks() { return new HalLinkExtractor(); @@ -114,7 +144,18 @@ public static LinkExtractor halLinks() { /** * Returns a {@code LinkExtractor} capable of extracting links in Atom format where - * the links are found in an array named {@code links}. + * the links are found in an array named {@code links}. For example: + * + *
+	 * {
+	 *     "links": [
+	 *         {
+	 *             "rel": "self",
+	 *             "href": "http://example.com/foo"
+	 *         }
+	 *     ]
+     * }
+	 * 
* * @return The extractor for Atom-style links */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 653442ed5..56f8266fd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -99,18 +99,18 @@ public static FieldDescriptor fieldWithPath(String path) { } /** - * Returns a handler that will produce a snippet documenting the fields of the API - * call's request. + * Returns a {@code Snippet} that will document the fields of the API operations's + * request payload. The fields will be documented using the given {@code descriptors}. *

- * 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 + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet 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 descriptions of the request's fields - * @return the handler + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields * @see #fieldWithPath(String) */ public static Snippet requestFields(FieldDescriptor... descriptors) { @@ -118,20 +118,20 @@ public static Snippet requestFields(FieldDescriptor... 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. + * Returns a {@code Snippet} that will document the fields of the API operation's + * request payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. *

- * 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. + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * payload, 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 + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields * @see #fieldWithPath(String) */ public static Snippet requestFields(Map attributes, @@ -140,18 +140,19 @@ public static Snippet requestFields(Map attributes, } /** - * Returns a handler that will produce a snippet documenting the fields of the API - * call's response. + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . *

- * 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. + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, 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 descriptions of the response's fields - * @return the handler + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields * @see #fieldWithPath(String) */ public static Snippet responseFields(FieldDescriptor... descriptors) { @@ -159,20 +160,20 @@ public static Snippet responseFields(FieldDescriptor... 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. + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. *

- * 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. + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, 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 + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields * @see #fieldWithPath(String) */ public static Snippet responseFields(Map attributes, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 345ec58cc..61c33d5fe 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -19,11 +19,8 @@ import java.util.Arrays; import java.util.Map; -import javax.servlet.ServletRequest; - +import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; /** * Static factory methods for documenting aspects of a request sent to a RESTful API. @@ -48,26 +45,36 @@ public static ParameterDescriptor parameterWithName(String name) { } /** - * Returns a snippet that will document the path parameters from the API call's - * request. + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

+ * If a parameter is present in the request path, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request path, a failure will also occur. * - * @param descriptors The descriptions of the parameters in the request's path - * @return the snippet - * @see PathVariable + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters */ public static Snippet pathParameters(ParameterDescriptor... descriptors) { return new PathParametersSnippet(Arrays.asList(descriptors)); } /** - * Returns a snippet that will document the path parameters from the API call's - * request. The given {@code attributes} will be available during snippet rendering. + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

+ * If a parameter is present in the request path, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request path, a failure will also occur. * - * @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 snippet - * @see PathVariable + * @param attributes the attributes + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters */ public static Snippet pathParameters(Map attributes, ParameterDescriptor... descriptors) { @@ -75,28 +82,38 @@ public static Snippet pathParameters(Map attributes, } /** - * Returns a snippet that will document the request parameters from the API call's - * request. + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

+ * If a parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. * * @param descriptors The descriptions of the request's parameters * @return the snippet - * @see RequestParam - * @see ServletRequest#getParameterMap() + * @see OperationRequest#getParameters() */ public static Snippet requestParameters(ParameterDescriptor... descriptors) { return new RequestParametersSnippet(Arrays.asList(descriptors)); } /** - * Returns a snippet that will document the request parameters from the API call's - * request. The given {@code attributes} will be available during snippet rendering. + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

+ * If a parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. * - * @param attributes Attributes made available during rendering of the request - * parameters snippet - * @param descriptors The descriptions of the request's parameters - * @return the snippet - * @see RequestParam - * @see ServletRequest#getParameterMap() + * @param attributes the attributes + * @param descriptors the descriptions of the request's parameters + * @return the snippet that will document the parameters + * @see OperationRequest#getParameters() */ public static Snippet requestParameters(Map attributes, ParameterDescriptor... descriptors) { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index a41ed1cf1..bedd3753a 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -51,10 +51,10 @@ public static RestDocumentationMockMvcConfigurer documentationConfiguration( /** * Documents the API call with the given {@code identifier} using the given - * {@code snippets}. + * {@code snippets} in addition to any default snippets. * * @param identifier an identifier for the API call that is being documented - * @param snippets the snippets that will document the API call + * @param snippets the snippets * @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) @@ -66,12 +66,12 @@ public static RestDocumentationResultHandler document(String identifier, /** * Documents the API call with the given {@code identifier} using the given - * {@code snippets}. The given {@code requestPreprocessor} is applied to the request - * before it is documented. + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} is applied to the request before it is documented. * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor - * @param snippets the snippets that will document the API call + * @param snippets the snippets * @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) @@ -84,12 +84,12 @@ public static RestDocumentationResultHandler document(String identifier, /** * Documents the API call with the given {@code identifier} using the given - * {@code snippets}. The given {@code responsePreprocessor} is applied to the request - * before it is documented. + * {@code snippets} in addition to any default snippets. The given + * {@code responsePreprocessor} is applied to the request before it is documented. * * @param identifier an identifier for the API call that is being documented * @param responsePreprocessor the response preprocessor - * @param snippets the snippets that will document the API call + * @param snippets the snippets * @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) @@ -102,14 +102,14 @@ public static RestDocumentationResultHandler document(String identifier, /** * Documents the API call with the given {@code identifier} using the given - * {@code snippets}. The given {@code requestPreprocessor} and - * {@code responsePreprocessor} are applied to the request and response respectively - * before they are documented. + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the + * request and response respectively before they are documented. * * @param identifier an identifier for the API call that is being documented * @param requestPreprocessor the request preprocessor * @param responsePreprocessor the response preprocessor - * @param snippets the snippets that will document the API call + * @param snippets the snippets * @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) diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java index 536f831d4..20f06771c 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java @@ -19,6 +19,8 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; @@ -33,6 +35,22 @@ /** * A {@link MockMvcConfigurer} that can be used to configure the documentation. + *

+ * In the absence of any {@link #snippets() customization} the following snippets will be + * produced by default: + *

    + *
  • {@link CurlDocumentation#curlRequest() Curl request}
  • + *
  • {@link HttpDocumentation#httpRequest() HTTP request}
  • + *
  • {@link HttpDocumentation#httpResponse() HTTP response}
  • + *
+ *

+ * In the absence of any {@link #uris() customization}, documented URIs have the following + * defaults: + *

    + *
  • Scheme: {@code http}
  • + *
  • Host: {@code localhost}
  • + *
  • Port: {@code 8080}
  • + *
* * @author Andy Wilkinson * @author Dmitriy Mayboroda From 48f4d1343ac2cecd1ad379ed485da5fdf2c39574 Mon Sep 17 00:00:00 2001 From: andreasevers Date: Sun, 20 Sep 2015 12:44:33 +0200 Subject: [PATCH 005/898] Add support for documenting HTTP headers This commit adds support for documenting HTTP headers in both requests and responses. Closes gh-140 --- config/checkstyle/checkstyle.xml | 2 +- .../src/main/asciidoc/api-guide.adoc | 7 + .../com/example/notes/ApiDocumentation.java | 11 ++ .../headers/AbstractHeadersSnippet.java | 147 ++++++++++++++++ .../restdocs/headers/HeaderDescriptor.java | 72 ++++++++ .../restdocs/headers/HeaderDocumentation.java | 116 ++++++++++++ .../headers/RequestHeadersSnippet.java | 63 +++++++ .../headers/ResponseHeadersSnippet.java | 63 +++++++ .../restdocs/headers/package-info.java | 20 +++ .../templates/default-request-headers.snippet | 9 + .../default-response-headers.snippet | 9 + .../headers/RequestHeadersSnippetTests.java | 164 +++++++++++++++++ .../headers/ResponseHeadersSnippetTests.java | 165 ++++++++++++++++++ .../restdocs/test/ExpectedSnippet.java | 10 ++ .../request-headers-with-extra-column.snippet | 10 ++ .../request-headers-with-title.snippet | 10 ++ ...response-headers-with-extra-column.snippet | 10 ++ .../response-headers-with-title.snippet | 10 ++ 18 files changed, 897 insertions(+), 1 deletion(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 117567d12..676822798 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc index 58c579bbc..c0a42afde 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc @@ -58,6 +58,13 @@ use of HTTP status codes. | The requested resource did not exist |=== +[[overview-headers]] +== Headers + +Every response has the following header(s): + +include::{snippets}/headers-example/response-headers.adoc[] + [[overview-errors]] == Errors diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 81a7fef7a..4da12b336 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -18,6 +18,8 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -98,6 +100,15 @@ public void setUp() { .alwaysDo(this.document) .build(); } + + @Test + public void headersExample() throws Exception { + this.document.snippets(responseHeaders( + headerWithName("Content-Type").description("The Content-Type of the payload, e.g. `application/hal+json`"))); + + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()); + } @Test public void errorExample() throws Exception { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java new file mode 100644 index 000000000..2d71e7fcd --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -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.headers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Assert; + +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response headers. + * + * @author Andreas Evers + */ +public abstract class AbstractHeadersSnippet extends TemplatedSnippet { + + private List headerDescriptors; + + private String type; + + /** + * Creates a new {@code AbstractHeadersSnippet} that will produce a snippet named + * {@code -headers}. The headers will be documented using the given + * {@code descriptors} and the given {@code attributes} will be included in the model + * during template rendering. + * + * @param type the type of the headers + * @param descriptors the header descriptors + * @param attributes the additional attributes + */ + protected AbstractHeadersSnippet(String type, List descriptors, + Map attributes) { + super(type + "-headers", attributes); + for (HeaderDescriptor descriptor : descriptors) { + Assert.notNull(descriptor.getName()); + Assert.notNull(descriptor.getDescription()); + } + this.headerDescriptors = descriptors; + this.type = type; + } + + @Override + protected Map createModel(Operation operation) { + validateHeaderDocumentation(operation); + + Map model = new HashMap<>(); + List> headers = new ArrayList<>(); + model.put("headers", headers); + for (HeaderDescriptor descriptor : this.headerDescriptors) { + headers.add(createModelForDescriptor(descriptor)); + } + return model; + } + + private void validateHeaderDocumentation(Operation operation) { + List missingHeaders = findMissingHeaders(operation); + + if (!missingHeaders.isEmpty()) { + String message = ""; + if (!missingHeaders.isEmpty()) { + List names = new ArrayList(); + for (HeaderDescriptor headerDescriptor : missingHeaders) { + names.add(headerDescriptor.getName()); + } + message += "Headers with the following names were not found in the " + + this.type + ": " + names; + } + throw new SnippetException(message); + } + } + + /** + * Finds the headers that are missing from the operation. A header is missing if + * it is described by one of the {@code headerDescriptors} but is not present in the + * operation. + * + * @param operation the operation + * @return descriptors for the headers that are missing from the operation + */ + protected List findMissingHeaders(Operation operation) { + List missingHeaders = new ArrayList(); + for (HeaderDescriptor headerDescriptor : this.headerDescriptors) { + if (!headerDescriptor.isOptional() + && !getHeaders(operation).contains(headerDescriptor.getName())) { + missingHeaders.add(headerDescriptor); + } + } + + return missingHeaders; + } + + /** + * Returns the headers of the request or response extracted form the given + * {@code operation}. + * + * @param operation The operation + * @return The headers + */ + protected abstract Set getHeaders(Operation operation); + + /** + * Returns the list of {@link HeaderDescriptor HeaderDescriptors} that will be used to + * generate the documentation. + * + * @return the header descriptors + */ + protected final List getHeaderDescriptors() { + return this.headerDescriptors; + } + + /** + * Returns a model for the given {@code descriptor}. + * + * @param descriptor the descriptor + * @return the model + */ + protected Map createModelForDescriptor(HeaderDescriptor descriptor) { + Map model = new HashMap(); + model.put("name", descriptor.getName()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java new file mode 100644 index 000000000..8e544bc2a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java @@ -0,0 +1,72 @@ +/* + * 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.headers; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.snippet.AbstractDescriptor; + +/** + * A description of a header found in a request or response. + * + * @author Andreas Evers + * @see HeaderDocumentation#headerWithName(String) + */ +public class HeaderDescriptor extends AbstractDescriptor { + + private final String name; + + private boolean optional; + + /** + * Creates a new {@code HeaderDescriptor} describing the header with the given + * {@code name}. + * @param name the name + * @see HttpHeaders + */ + protected HeaderDescriptor(String name) { + this.name = name; + } + + /** + * Marks the header as optional. + * + * @return {@code this} + */ + public final HeaderDescriptor optional() { + this.optional = true; + return this; + } + + /** + * Returns the name for the header. + * + * @return the header name + */ + public String getName() { + return this.name; + } + + /** + * Returns {@code true} if the described header is optional, otherwise {@code false}. + * + * @return {@code true} if the described header is optional, otherwise {@code false} + */ + public final boolean isOptional() { + return this.optional; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java new file mode 100644 index 000000000..a7d5afc6a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -0,0 +1,116 @@ +/* + * 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.headers; + +import java.util.Arrays; +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API's request and response headers. + * + * @author Andreas Evers + */ +public abstract class HeaderDocumentation { + + private HeaderDocumentation() { + + } + + /** + * Creates a {@code HeaderDescriptor} that describes a header with the given + * {@code name}. + * + * @param name The name of the header + * @return a {@code HeaderDescriptor} ready for further configuration + */ + public static HeaderDescriptor headerWithName(String name) { + return new HeaderDescriptor(name); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's request. + *

+ * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. If a header is present in the request, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param descriptors The descriptions of the request's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet requestHeaders(HeaderDescriptor... descriptors) { + return new RequestHeadersSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's request. The given {@code attributes} will be available during snippet + * generation. + *

+ * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. If a header is present in the request, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param attributes Attributes made available during rendering of the snippet + * @param descriptors The descriptions of the request's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet requestHeaders(Map attributes, + HeaderDescriptor... descriptors) { + return new RequestHeadersSnippet(Arrays.asList(descriptors), attributes); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's response. + *

+ * If a header is documented, is not marked as optional, and is not present in the + * response, a failure will occur. If a header is present in the response, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param descriptors The descriptions of the response's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet responseHeaders(HeaderDescriptor... descriptors) { + return new ResponseHeadersSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a handler that will produce a snippet documenting the headers of the API + * call's response. The given {@code attributes} will be available during snippet + * generation. + *

+ * If a header is documented, is not marked as optional, and is not present in the + * response, a failure will occur. If a header is present in the response, but is not + * documented by one of the descriptors, there will be no failure. + * + * @param attributes Attributes made available during rendering of the snippet + * @param descriptors The descriptions of the response's headers + * @return the handler + * @see #headerWithName(String) + */ + public static Snippet responseHeaders(Map attributes, + HeaderDescriptor... descriptors) { + return new ResponseHeadersSnippet(Arrays.asList(descriptors), attributes); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java new file mode 100644 index 000000000..deb7efca4 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -0,0 +1,63 @@ +/* + * 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.headers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the headers in a request. + * + * @author Andreas Evers + * @see HeaderDocumentation#requestHeaders(HeaderDescriptor...) + * @see HeaderDocumentation#requestHeaders(Map, HeaderDescriptor...) + */ +public class RequestHeadersSnippet extends AbstractHeadersSnippet { + + /** + * Creates a new {@code RequestHeadersSnippet} that will document the headers in the + * request using the given {@code descriptors}. + * + * @param descriptors the descriptors + */ + protected RequestHeadersSnippet(List descriptors) { + this(descriptors, null); + } + + /** + * Creates a new {@code RequestHeadersSnippet} that will document the headers in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestHeadersSnippet(List descriptors, + Map attributes) { + super("request", descriptors, attributes); + } + + @Override + protected Set getHeaders(Operation operation) { + return operation.getRequest().getHeaders().keySet(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java new file mode 100644 index 000000000..bb0d5d079 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -0,0 +1,63 @@ +/* + * 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.headers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the headers in a response. + * + * @author Andreas Evers + * @see HeaderDocumentation#responseHeaders(HeaderDescriptor...) + * @see HeaderDocumentation#responseHeaders(Map, HeaderDescriptor...) + */ +public class ResponseHeadersSnippet extends AbstractHeadersSnippet { + + /** + * Creates a new {@code ResponseHeadersSnippet} that will document the headers in the + * response using the given {@code descriptors}. + * + * @param descriptors the descriptors + */ + protected ResponseHeadersSnippet(List descriptors) { + this(descriptors, null); + } + + /** + * Creates a new {@code ResponseHeadersSnippet} that will document the headers in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected ResponseHeadersSnippet(List descriptors, + Map attributes) { + super("response", descriptors, attributes); + } + + @Override + protected Set getHeaders(Operation operation) { + return operation.getResponse().getHeaders().keySet(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java new file mode 100644 index 000000000..f27ba176c --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting the headers of a RESTful API's requests and responses. + */ +package org.springframework.restdocs.headers; diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet new file mode 100644 index 000000000..5f14875dc --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet new file mode 100644 index 000000000..5f14875dc --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet @@ -0,0 +1,9 @@ +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java new file mode 100644 index 000000000..93e831366 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -0,0 +1,164 @@ +/* + * 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.headers; + +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.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; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + +/** + * Tests for {@link RequestHeadersSnippet}. + * + * @author Andreas Evers + */ +public class RequestHeadersSnippetTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void requestWithHeaders() throws IOException { + this.snippet.expectRequestHeaders("request-with-headers").withContents(// + tableWithHeader("Name", "Description") // + .row("X-Test", "one") // + .row("Accept", "two") // + .row("Accept-Encoding", "three") // + .row("Accept-Language", "four") // + .row("Cache-Control", "five") // + .row("Connection", "six")); + new RequestHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one"), // + headerWithName("Accept").description("two"), // + headerWithName("Accept-Encoding").description("three"), // + headerWithName("Accept-Language").description("four"), // + headerWithName("Cache-Control").description("five"), // + headerWithName("Connection").description("six"))) // + .document(new OperationBuilder("request-with-headers", this.snippet + .getOutputDirectory()) // + .request("http://localhost") // + .header("X-Test", "test") // + .header("Accept", "*/*") // + .header("Accept-Encoding", "gzip, deflate") // + .header("Accept-Language", "en-US,en;q=0.5") // + .header("Cache-Control", "max-age=0") // + .header("Connection", "keep-alive") // + .build()); + } + + @Test + public void undocumentedRequestHeader() throws IOException { + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder("undocumented-request-header", + this.snippet.getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*").build()); + } + + @Test + public void missingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder("missing-request-headers", + this.snippet.getOutputDirectory()).request("http://localhost").build()); + } + + @Test + public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder( + "undocumented-request-header-and-missing-request-header", this.snippet + .getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").build()); + } + + @Test + public void requestHeadersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") + .withContents(// + tableWithHeader("Name", "Description", "Foo") // + .row("X-Test", "one", "alpha") // + .row("Accept-Encoding", "two", "bravo") // + .row("Accept", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-headers")).willReturn( + snippetResource("request-headers-with-extra-column")); + new RequestHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one").attributes( + key("foo").value("alpha")), + headerWithName("Accept-Encoding").description("two").attributes( + key("foo").value("bravo")), + headerWithName("Accept").description("three").attributes( + key("foo").value("charlie")))).document(new OperationBuilder( + "request-headers-with-custom-attributes", this.snippet + .getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).request("http://localhost") + .header("X-Test", "test").header("Accept-Encoding", "gzip, deflate") + .header("Accept", "*/*").build()); + } + + @Test + public void requestHeadersWithCustomAttributes() throws IOException { + this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-headers")).willReturn( + snippetResource("request-headers-with-title")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(new OperationBuilder("request-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").header("X-Test", "test").build()); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java new file mode 100644 index 000000000..ac6a40cee --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -0,0 +1,165 @@ +/* + * 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.headers; + +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.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; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + +/** + * Tests for {@link ReponseHeadersSnippet}. + * + * @author Andreas Evers + */ +public class ResponseHeadersSnippetTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final ExpectedSnippet snippet = new ExpectedSnippet(); + + @Test + public void responseWithHeaders() throws IOException { + this.snippet.expectResponseHeaders("response-headers").withContents(// + tableWithHeader("Name", "Description") // + .row("X-Test", "one") // + .row("Content-Type", "two") // + .row("Etag", "three") // + .row("Content-Length", "four") // + .row("Cache-Control", "five") // + .row("Vary", "six")); + new ResponseHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one"), // + headerWithName("Content-Type").description("two"), // + headerWithName("Etag").description("three"), // + headerWithName("Content-Length").description("four"), // + headerWithName("Cache-Control").description("five"), // + headerWithName("Vary").description("six"))) // + .document(new OperationBuilder("response-headers", this.snippet + .getOutputDirectory()) // + .response() // + .header("X-Test", "test") // + .header("Content-Type", "application/json") // + .header("Etag", "lskjadldj3ii32l2ij23") // + .header("Content-Length", "19166") // + .header("Cache-Control", "max-age=0") // + .header("Vary", "User-Agent") // + .build()); + } + + @Test + public void responseHeadersWithCustomDescriptorAttributes() throws IOException { + this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + .withContents(// + tableWithHeader("Name", "Description", "Foo") // + .row("X-Test", "one", "alpha") // + .row("Content-Type", "two", "bravo") // + .row("Etag", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-headers")).willReturn( + snippetResource("response-headers-with-extra-column")); + new ResponseHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one").attributes( + key("foo").value("alpha")), + headerWithName("Content-Type").description("two").attributes( + key("foo").value("bravo")), + headerWithName("Etag").description("three").attributes( + key("foo").value("charlie")))).document(new OperationBuilder( + "response-headers-with-custom-attributes", this.snippet + .getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .header("X-Test", "test").header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23").build()); + } + + @Test + public void responseHeadersWithCustomAttributes() throws IOException { + this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + .withContents(startsWith(".Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-headers")).willReturn( + snippetResource("response-headers-with-title")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(new OperationBuilder("response-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .header("X-Test", "test").build()); + } + + @Test + public void undocumentedResponseHeader() throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder("undocumented-response-header", + this.snippet.getOutputDirectory()).response().header("X-Test", "test") + .header("Content-Type", "*/*").build()); + } + + @Test + public void missingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "missing-response-headers", this.snippet.getOutputDirectory()).response() + .build()); + } + + @Test + public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "undocumented-response-header-and-missing-response-header", this.snippet + .getOutputDirectory()).response().header("X-Test", "test") + .build()); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index c20b0df06..475cb15c1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -75,6 +75,16 @@ public ExpectedSnippet expectResponseFields(String name) { return this; } + public ExpectedSnippet expectRequestHeaders(String name) { + expect(name, "request-headers"); + return this; + } + + public ExpectedSnippet expectResponseHeaders(String name) { + expect(name, "response-headers"); + return this; + } + public ExpectedSnippet expectLinks(String name) { expect(name, "links"); return this; diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet new file mode 100644 index 000000000..29d687777 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#headers}} +|{{name}} +|{{description}} +|{{foo}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet new file mode 100644 index 000000000..998090a0d --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet new file mode 100644 index 000000000..29d687777 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Name|Description|Foo + +{{#headers}} +|{{name}} +|{{description}} +|{{foo}} + +{{/headers}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet new file mode 100644 index 000000000..998090a0d --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Name|Description + +{{#headers}} +|{{name}} +|{{description}} + +{{/headers}} +|=== \ No newline at end of file From ea8f736259ea9095ce41a84b15f848b2133040b0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 29 Sep 2015 12:31:04 +0100 Subject: [PATCH 006/898] Polish contribution that added support for documenting HTTP headers The main changes are: - Update javadoc to align with changes made in 5a01016 - Update documentation with details of the new support for documenting HTTP headers - Add tests to verify that HTTP headers are matched case-insensitively Closes gh-71 --- config/checkstyle/checkstyle.xml | 2 +- .../docs/asciidoc/documenting-your-api.adoc | 25 ++++++++ .../test/java/com/example/HttpHeaders.java | 50 ++++++++++++++++ .../headers/AbstractHeadersSnippet.java | 15 ++--- .../restdocs/headers/HeaderDescriptor.java | 2 +- .../restdocs/headers/HeaderDocumentation.java | 54 +++++++++-------- .../headers/RequestHeadersSnippet.java | 2 +- .../headers/ResponseHeadersSnippet.java | 2 +- .../headers/RequestHeadersSnippetTests.java | 56 ++++++++++-------- .../headers/ResponseHeadersSnippetTests.java | 58 ++++++++++--------- .../restdocs/test/ExpectedSnippet.java | 1 + 11 files changed, 175 insertions(+), 92 deletions(-) create mode 100644 docs/src/test/java/com/example/HttpHeaders.java diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 676822798..b7908d746 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 7fd84cdf3..af979cb05 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -288,6 +288,31 @@ built using one of the methods on `RestDocumentationRequestBuilders` rather than +[[documenting-your-api-http-headers]] +=== HTTP headers + +The headers in a request or response can be documented using `requestHeaders` and +`responseHeaders` respectively. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/HttpHeaders.java[tags=headers] +---- +<1> Perform a `GET` request with an `Authorization` header that uses basic authentication +<2> Produce a snippet describing the request's headers. Uses the static `requestHeaders` +method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<3> Document the `Authorization` header. Uses the static `headerWithName` method on +`org.springframework.restdocs.headers.HeaderDocumentation. +<4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` +method on `org.springframework.restdocs.headers.HeaderDocumentation`. + +The result is a snippet named `request-headers.adoc` and a snippet named +`response-headers.adoc`. Each contains a table describing the headers. + +When documenting HTTP Headers, the test will fail if a documented header is not found in +the request or response. + + [[documenting-your-api-constraints]] === Documenting constraints diff --git a/docs/src/test/java/com/example/HttpHeaders.java b/docs/src/test/java/com/example/HttpHeaders.java new file mode 100644 index 000000000..f47e39714 --- /dev/null +++ b/docs/src/test/java/com/example/HttpHeaders.java @@ -0,0 +1,50 @@ +/* + * 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 com.example; + +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class HttpHeaders { + + private MockMvc mockMvc; + + public void headers() throws Exception { + // tag::headers[] + this.mockMvc + .perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // <1> + .andExpect(status().isOk()) + .andDo(document("headers", + requestHeaders( // <2> + headerWithName("Authorization").description( + "Basic auth credentials")), // <3> + responseHeaders( // <4> + headerWithName("X-RateLimit-Limit").description( + "The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining").description( + "Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset").description( + "Time at which the rate limit period will reset")))); + // end::headers[] + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java index 2d71e7fcd..5c3099b9d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -91,8 +91,8 @@ private void validateHeaderDocumentation(Operation operation) { } /** - * Finds the headers that are missing from the operation. A header is missing if - * it is described by one of the {@code headerDescriptors} but is not present in the + * Finds the headers that are missing from the operation. A header is missing if it is + * described by one of the {@code headerDescriptors} but is not present in the * operation. * * @param operation the operation @@ -100,9 +100,10 @@ private void validateHeaderDocumentation(Operation operation) { */ protected List findMissingHeaders(Operation operation) { List missingHeaders = new ArrayList(); + Set actualHeaders = extractActualHeaders(operation); for (HeaderDescriptor headerDescriptor : this.headerDescriptors) { if (!headerDescriptor.isOptional() - && !getHeaders(operation).contains(headerDescriptor.getName())) { + && !actualHeaders.contains(headerDescriptor.getName())) { missingHeaders.add(headerDescriptor); } } @@ -111,13 +112,13 @@ protected List findMissingHeaders(Operation operation) { } /** - * Returns the headers of the request or response extracted form the given + * Extracts the names of the headers from the request or response of the given * {@code operation}. * - * @param operation The operation - * @return The headers + * @param operation the operation + * @return the header names */ - protected abstract Set getHeaders(Operation operation); + protected abstract Set extractActualHeaders(Operation operation); /** * Returns the list of {@link HeaderDescriptor HeaderDescriptors} that will be used to diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java index 8e544bc2a..b0e332abf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDescriptor.java @@ -56,7 +56,7 @@ public final HeaderDescriptor optional() { * * @return the header name */ - public String getName() { + public final String getName() { return this.name; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java index a7d5afc6a..85800ecff 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -25,6 +25,7 @@ * Static factory methods for documenting a RESTful API's request and response headers. * * @author Andreas Evers + * @author Andy Wilkinson */ public abstract class HeaderDocumentation { @@ -44,15 +45,14 @@ public static HeaderDescriptor headerWithName(String name) { } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's request. + * Returns a new {@link Snippet} that will document the headers of the API operation's + * request. The headers will be documented using the given {@code descriptors}. *

* If a header is documented, is not marked as optional, and is not present in the - * request, a failure will occur. If a header is present in the request, but is not - * documented by one of the descriptors, there will be no failure. + * request, a failure will occur. * - * @param descriptors The descriptions of the request's headers - * @return the handler + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers * @see #headerWithName(String) */ public static Snippet requestHeaders(HeaderDescriptor... descriptors) { @@ -60,17 +60,16 @@ public static Snippet requestHeaders(HeaderDescriptor... descriptors) { } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's request. The given {@code attributes} will be available during snippet - * generation. + * Returns a new {@link Snippet} that will document the headers of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the headers will be documented using the given {@code descriptors}. *

* If a header is documented, is not marked as optional, and is not present in the - * request, a failure will occur. If a header is present in the request, but is not - * documented by one of the descriptors, there will be no failure. + * request, a failure will occur. * - * @param attributes Attributes made available during rendering of the snippet - * @param descriptors The descriptions of the request's headers - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers * @see #headerWithName(String) */ public static Snippet requestHeaders(Map attributes, @@ -79,15 +78,14 @@ public static Snippet requestHeaders(Map attributes, } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's response. + * Returns a new {@link Snippet} that will document the headers of the API operation's + * response. The headers will be documented using the given {@code descriptors}. *

* If a header is documented, is not marked as optional, and is not present in the - * response, a failure will occur. If a header is present in the response, but is not - * documented by one of the descriptors, there will be no failure. + * request, a failure will occur. * - * @param descriptors The descriptions of the response's headers - * @return the handler + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers * @see #headerWithName(String) */ public static Snippet responseHeaders(HeaderDescriptor... descriptors) { @@ -95,17 +93,17 @@ public static Snippet responseHeaders(HeaderDescriptor... descriptors) { } /** - * Returns a handler that will produce a snippet documenting the headers of the API - * call's response. The given {@code attributes} will be available during snippet - * generation. + * Returns a new {@link Snippet} that will document the headers of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the headers will be documented using the given + * {@code descriptors}. *

* If a header is documented, is not marked as optional, and is not present in the - * response, a failure will occur. If a header is present in the response, but is not - * documented by one of the descriptors, there will be no failure. + * response, a failure will occur. * - * @param attributes Attributes made available during rendering of the snippet - * @param descriptors The descriptions of the response's headers - * @return the handler + * @param attributes the attributes + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers * @see #headerWithName(String) */ public static Snippet responseHeaders(Map attributes, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java index deb7efca4..83dd6b6d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -56,7 +56,7 @@ protected RequestHeadersSnippet(List descriptors, } @Override - protected Set getHeaders(Operation operation) { + protected Set extractActualHeaders(Operation operation) { return operation.getRequest().getHeaders().keySet(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java index bb0d5d079..979083c42 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -56,7 +56,7 @@ protected ResponseHeadersSnippet(List descriptors, } @Override - protected Set getHeaders(Operation operation) { + protected Set extractActualHeaders(Operation operation) { return operation.getResponse().getHeaders().keySet(); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 93e831366..b5b2d1e4c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -44,6 +44,7 @@ * Tests for {@link RequestHeadersSnippet}. * * @author Andreas Evers + * @author Andy Wilkinson */ public class RequestHeadersSnippetTests { @@ -55,31 +56,36 @@ public class RequestHeadersSnippetTests { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectRequestHeaders("request-with-headers").withContents(// - tableWithHeader("Name", "Description") // - .row("X-Test", "one") // - .row("Accept", "two") // - .row("Accept-Encoding", "three") // - .row("Accept-Language", "four") // - .row("Cache-Control", "five") // + this.snippet.expectRequestHeaders("request-with-headers").withContents( + tableWithHeader("Name", "Description").row("X-Test", "one") + .row("Accept", "two").row("Accept-Encoding", "three") + .row("Accept-Language", "four").row("Cache-Control", "five") .row("Connection", "six")); new RequestHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one"), // - headerWithName("Accept").description("two"), // - headerWithName("Accept-Encoding").description("three"), // - headerWithName("Accept-Language").description("four"), // - headerWithName("Cache-Control").description("five"), // - headerWithName("Connection").description("six"))) // + headerWithName("X-Test").description("one"), headerWithName("Accept") + .description("two"), headerWithName("Accept-Encoding") + .description("three"), headerWithName("Accept-Language") + .description("four"), headerWithName("Cache-Control") + .description("five"), + headerWithName("Connection").description("six"))) .document(new OperationBuilder("request-with-headers", this.snippet - .getOutputDirectory()) // - .request("http://localhost") // - .header("X-Test", "test") // - .header("Accept", "*/*") // - .header("Accept-Encoding", "gzip, deflate") // - .header("Accept-Language", "en-US,en;q=0.5") // - .header("Cache-Control", "max-age=0") // - .header("Connection", "keep-alive") // - .build()); + .getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive").build()); + } + + @Test + public void caseInsensitiveRequestHeaders() throws IOException { + this.snippet + .expectRequestHeaders("case-insensitive-request-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder( + "case-insensitive-request-headers", this.snippet.getOutputDirectory()) + .request("/").header("X-test", "test").build()); } @Test @@ -118,9 +124,9 @@ public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOExceptio public void requestHeadersWithCustomDescriptorAttributes() throws IOException { this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") .withContents(// - tableWithHeader("Name", "Description", "Foo") // - .row("X-Test", "one", "alpha") // - .row("Accept-Encoding", "two", "bravo") // + tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Accept-Encoding", "two", "bravo") .row("Accept", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")).willReturn( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index ac6a40cee..24487094f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -41,9 +41,10 @@ import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** - * Tests for {@link ReponseHeadersSnippet}. + * Tests for {@link ResponseHeadersSnippet}. * * @author Andreas Evers + * @author Andy Wilkinson */ public class ResponseHeadersSnippetTests { @@ -55,40 +56,41 @@ public class ResponseHeadersSnippetTests { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders("response-headers").withContents(// - tableWithHeader("Name", "Description") // - .row("X-Test", "one") // - .row("Content-Type", "two") // - .row("Etag", "three") // - .row("Content-Length", "four") // - .row("Cache-Control", "five") // - .row("Vary", "six")); + this.snippet.expectResponseHeaders("response-headers").withContents( + tableWithHeader("Name", "Description").row("X-Test", "one") + .row("Content-Type", "two").row("Etag", "three") + .row("Cache-Control", "five").row("Vary", "six")); new ResponseHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one"), // - headerWithName("Content-Type").description("two"), // - headerWithName("Etag").description("three"), // - headerWithName("Content-Length").description("four"), // - headerWithName("Cache-Control").description("five"), // - headerWithName("Vary").description("six"))) // + headerWithName("X-Test").description("one"), + headerWithName("Content-Type").description("two"), headerWithName("Etag") + .description("three"), headerWithName("Cache-Control") + .description("five"), headerWithName("Vary").description("six"))) .document(new OperationBuilder("response-headers", this.snippet - .getOutputDirectory()) // - .response() // - .header("X-Test", "test") // - .header("Content-Type", "application/json") // - .header("Etag", "lskjadldj3ii32l2ij23") // - .header("Content-Length", "19166") // - .header("Cache-Control", "max-age=0") // - .header("Vary", "User-Agent") // - .build()); + .getOutputDirectory()).response().header("X-Test", "test") + .header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0") + .header("Vary", "User-Agent").build()); + } + + @Test + public void caseInsensitiveResponseHeaders() throws IOException { + this.snippet + .expectResponseHeaders("case-insensitive-response-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder( + "case-insensitive-response-headers", this.snippet.getOutputDirectory()) + .response().header("X-test", "test").build()); } @Test public void responseHeadersWithCustomDescriptorAttributes() throws IOException { this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") - .withContents(// - tableWithHeader("Name", "Description", "Foo") // - .row("X-Test", "one", "alpha") // - .row("Content-Type", "two", "bravo") // + .withContents( + tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Content-Type", "two", "bravo") .row("Etag", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-headers")).willReturn( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 475cb15c1..63364f6a6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -34,6 +34,7 @@ * generated the expected snippet. * * @author Andy Wilkinson + * @author Andreas Evers */ public class ExpectedSnippet implements TestRule { From fb01a26a16fd267f7c5de546b69358666d7fe728 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 30 Sep 2015 17:33:29 +0100 Subject: [PATCH 007/898] Allow an item to be undocumented without it causing a test failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, it was necessary to document every payload field, link, path parameter, or request. If an item was not documented a failure would occur. This has proven to be too restrictive for some use cases, for example splitting up the documentation of a payload’s fields. While it was possible to use a preprocessor to modify the operation prior to documentation to remove the items that should not be documented, this was more difficult than it needed to be. This commit adds support for marking a descriptor as ignored. Ignored descriptors count when checking that everything has been documented but do not actually appear in the generated documentation. Closes gh-143 --- .../hypermedia/HypermediaDocumentation.java | 16 ++++++ .../restdocs/hypermedia/LinkDescriptor.java | 4 +- .../restdocs/hypermedia/LinksSnippet.java | 13 +++-- .../payload/AbstractFieldsSnippet.java | 13 +++-- .../restdocs/payload/FieldDescriptor.java | 4 +- .../payload/PayloadDocumentation.java | 16 ++++++ .../request/AbstractParametersSnippet.java | 14 +++-- .../restdocs/request/ParameterDescriptor.java | 4 +- .../request/RequestDocumentation.java | 16 ++++++ .../restdocs/snippet/IgnorableDescriptor.java | 52 +++++++++++++++++++ .../hypermedia/LinksSnippetTests.java | 11 ++++ .../payload/RequestFieldsSnippetTests.java | 13 +++++ .../payload/ResponseFieldsSnippetTests.java | 13 +++++ .../request/PathParametersSnippetTests.java | 12 +++++ .../RequestParametersSnippetTests.java | 11 ++++ ...kMvcRestDocumentationIntegrationTests.java | 29 +++++++++++ 16 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index c073484e3..a4fbf42d4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -51,6 +51,10 @@ public static LinkDescriptor linkWithRel(String rel) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link * is documented, is not marked as optional, and is not present in the response, a * failure will also occur. + *

+ * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param descriptors the descriptions of the response's links * @return the snippet that will document the links @@ -70,6 +74,10 @@ public static Snippet links(LinkDescriptor... descriptors) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link * is documented, is not marked as optional, and is not present in the response, a * failure will also occur. + *

+ * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the response's links @@ -90,6 +98,10 @@ public static Snippet links(Map attributes, * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link * is documented, is not marked as optional, and is not present in the response, a * failure will also occur. + *

+ * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links @@ -110,6 +122,10 @@ public static Snippet links(LinkExtractor linkExtractor, * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link * is documented, is not marked as optional, and is not present in the response, a * failure will also occur. + *

+ * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param linkExtractor used to extract the links from the response diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index 810a52448..8876cf7de 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.hypermedia; -import org.springframework.restdocs.snippet.AbstractDescriptor; +import org.springframework.restdocs.snippet.IgnorableDescriptor; /** * A description of a link found in a hypermedia API. @@ -24,7 +24,7 @@ * @author Andy Wilkinson * @see HypermediaDocumentation#linkWithRel(String) */ -public class LinkDescriptor extends AbstractDescriptor { +public class LinkDescriptor extends IgnorableDescriptor { private final String rel; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 8b5a04d46..79ae0ee26 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -74,8 +74,12 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip super("links", attributes); this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { - Assert.hasText(descriptor.getRel()); - Assert.notNull(descriptor.getDescription()); + Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), "The descriptor for link '" + + descriptor.getRel() + "' must either have a description or be" + + " marked as " + "ignored"); + } this.descriptorsByRel.put(descriptor.getRel(), descriptor); } } @@ -131,7 +135,10 @@ private void validate(Map> links) { private List> createLinksModel() { List> model = new ArrayList<>(); for (Entry entry : this.descriptorsByRel.entrySet()) { - model.add(createModelForDescriptor(entry.getValue())); + LinkDescriptor descriptor = entry.getValue(); + if (!descriptor.isIgnored()) { + model.add(createModelForDescriptor(descriptor)); + } } return model; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index c96b58f49..3bf98a83b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -55,8 +55,13 @@ protected AbstractFieldsSnippet(String type, List descriptors, Map attributes) { super(type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { - Assert.notNull(descriptor.getPath()); - Assert.notNull(descriptor.getDescription()); + Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), "The descriptor for field '" + + descriptor.getPath() + "' must either have a description or" + + " be marked as " + "ignored"); + } + } this.fieldDescriptors = descriptors; } @@ -77,7 +82,9 @@ protected Map createModel(Operation operation) { List> fields = new ArrayList<>(); model.put("fields", fields); for (FieldDescriptor descriptor : this.fieldDescriptors) { - fields.add(createModelForDescriptor(descriptor)); + if (!descriptor.isIgnored()) { + fields.add(createModelForDescriptor(descriptor)); + } } return model; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 7df08f1fd..d68bb68d7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.payload; -import org.springframework.restdocs.snippet.AbstractDescriptor; +import org.springframework.restdocs.snippet.IgnorableDescriptor; /** * A description of a field found in a request or response payload. @@ -25,7 +25,7 @@ * @author Andy Wilkinson * @see PayloadDocumentation#fieldWithPath(String) */ -public class FieldDescriptor extends AbstractDescriptor { +public class FieldDescriptor extends IgnorableDescriptor { private final String path; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 56f8266fd..097e1c47c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -108,6 +108,10 @@ public static FieldDescriptor fieldWithPath(String path) { * 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. + *

+ * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields @@ -128,6 +132,10 @@ public static Snippet requestFields(FieldDescriptor... descriptors) { * payload, 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. + *

+ * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields @@ -150,6 +158,10 @@ public static Snippet requestFields(Map attributes, * payload, 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. + *

+ * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields @@ -170,6 +182,10 @@ public static Snippet responseFields(FieldDescriptor... descriptors) { * payload, 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. + *

+ * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the response payload's fields diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 144c8d672..0763505b1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -54,8 +54,13 @@ protected AbstractParametersSnippet(String snippetName, List descriptors, Map attributes) { super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { - Assert.hasText(descriptor.getName()); - Assert.notNull(descriptor.getDescription()); + Assert.notNull(descriptor.getName(), "Parameter descriptors must have a name"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), + "The descriptor for parameter '" + descriptor.getName() + + "' must either have a description or be marked as " + + "ignored"); + } this.descriptorsByName.put(descriptor.getName(), descriptor); } } @@ -67,7 +72,10 @@ protected Map createModel(Operation operation) { Map model = new HashMap<>(); List> parameters = new ArrayList<>(); for (Entry entry : this.descriptorsByName.entrySet()) { - parameters.add(createModelForDescriptor(entry.getValue())); + ParameterDescriptor descriptor = entry.getValue(); + if (!descriptor.isIgnored()) { + parameters.add(createModelForDescriptor(descriptor)); + } } model.put("parameters", parameters); return model; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index ff9d561dd..9172a5525 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.request; -import org.springframework.restdocs.snippet.AbstractDescriptor; +import org.springframework.restdocs.snippet.IgnorableDescriptor; /** * A descriptor of a request or path parameter. @@ -25,7 +25,7 @@ * @see RequestDocumentation#parameterWithName * */ -public class ParameterDescriptor extends AbstractDescriptor { +public class ParameterDescriptor extends IgnorableDescriptor { private final String name; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 61c33d5fe..e0aa2f4ba 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -53,6 +53,10 @@ public static ParameterDescriptor parameterWithName(String name) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * parameter is documented, is not marked as optional, and is not present in the * request path, a failure will also occur. + *

+ * If you do not want to document a path parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. * * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters @@ -71,6 +75,10 @@ public static Snippet pathParameters(ParameterDescriptor... descriptors) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * parameter is documented, is not marked as optional, and is not present in the * request path, a failure will also occur. + *

+ * If you do not want to document a path parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the parameters in the request's path @@ -90,6 +98,10 @@ public static Snippet pathParameters(Map attributes, * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. + *

+ * If you do not want to document a request parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. * * @param descriptors The descriptions of the request's parameters * @return the snippet @@ -109,6 +121,10 @@ public static Snippet requestParameters(ParameterDescriptor... descriptors) { * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * parameter is documented, is not marked as optional, and is not present in the * request, a failure will also occur. + *

+ * If you do not want to document a request parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. * * @param attributes the attributes * @param descriptors the descriptions of the request's parameters diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java new file mode 100644 index 000000000..8174c3815 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * Base class for descriptors for items that can be ignored. + * + * @param the type of the descriptor + * @author Andy Wilkinson + */ +public abstract class IgnorableDescriptor> extends + AbstractDescriptor { + + private boolean ignored = false; + + /** + * Marks the described item as being ignored. Ignored items are not included in the + * generated documentation. + * + * @return the descriptor + */ + @SuppressWarnings("unchecked") + public final T ignored() { + this.ignored = true; + return (T) this; + } + + /** + * Returns whether or not the item being described should be ignored and, therefore, + * should not be included in the documentation. + * + * @return {@code true} if the item should be ignored, otherwise {@code false}. + */ + public final boolean isIgnored() { + return this.ignored; + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 57b9f7556..cb9ac2883 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -65,6 +65,17 @@ public void undocumentedLink() throws IOException { "undocumented-link", this.snippet.getOutputDirectory()).build()); } + @Test + public void ignoredLink() throws IOException { + this.snippet.expectLinks("ignored-link").withContents( + tableWithHeader("Relation", "Description").row("b", "Link b")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(), + new LinkDescriptor("b").description("Link b"))) + .document(new OperationBuilder("ignored-link", this.snippet + .getOutputDirectory()).build()); + } + @Test public void missingLink() throws IOException { this.thrown.expect(SnippetException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 1c1481c80..0fedbdee4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -98,6 +98,19 @@ public void undocumentedRequestField() throws IOException { .content("{\"a\": 5}").build()); } + @Test + public void ignoredRequestField() throws IOException { + this.snippet.expectRequestFields("ignored-request-field").withContents( + tableWithHeader("Path", "Type", "Description").row("b", "Number", + "Field b")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), + fieldWithPath("b").description("Field b"))) + .document(new OperationBuilder("ignored-request-field", this.snippet + .getOutputDirectory()).request("http://localhost") + .content("{\"a\": 5, \"b\": 4}").build()); + } + @Test public void missingRequestField() throws IOException { this.thrown.expect(SnippetException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 56bf56886..337e6e8c6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -106,6 +106,19 @@ public void arrayResponse() throws IOException { .content("[\"a\", \"b\", \"c\"]").build()); } + @Test + public void ignoredResponseField() throws IOException { + this.snippet.expectResponseFields("ignored-response-field").withContents( + tableWithHeader("Path", "Type", "Description").row("b", "Number", + "Field b")); + + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), + fieldWithPath("b").description("Field b"))) + .document(new OperationBuilder("ignored-response-field", this.snippet + .getOutputDirectory()).response().content("{\"a\": 5, \"b\": 4}") + .build()); + } + @Test public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index f7f0f462c..4f2ee0b23 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -104,6 +104,18 @@ public void pathParameters() throws IOException { "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); } + @Test + public void ignoredPathParameter() throws IOException { + this.snippet.expectPathParameters("ignored-path-parameter").withContents( + tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", + "two")); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), + parameterWithName("b").description("two"))) + .document(new OperationBuilder("ignored-path-parameter", this.snippet + .getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + } + @Test public void pathParametersWithQueryString() throws IOException { this.snippet.expectPathParameters("path-parameters-with-query-string") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 9ac769721..da61deecc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -102,6 +102,17 @@ public void requestParameters() throws IOException { .build()); } + @Test + public void ignoredRequestParameter() throws IOException { + this.snippet.expectRequestParameters("ignored-request-parameter").withContents( + tableWithHeader("Parameter", "Description").row("b", "two")); + new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), + parameterWithName("b").description("two"))) + .document(new OperationBuilder("ignored-request-parameter", this.snippet + .getOutputDirectory()).request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); + } + @Test public void requestParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index cbf725447..8c5be89e5 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -400,6 +400,30 @@ public void customContextPath() throws Exception { "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } + @Test + public void stackOverflowQuestion() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(get("/company/5").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document( + "company", + responseFields( + fieldWithPath("companyName").description( + "The name of the company"), + fieldWithPath("employee").description( + "An array of the company's employees")))) + .andDo(document( + "employee", + responseFields( + fieldWithPath("companyName").ignored(), + fieldWithPath("employee[].name").description( + "The name of the employee"), + fieldWithPath("employee[].age").description( + "The age of the employee")))); + } + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { assertTrue(new File(directory, snippet).isFile()); @@ -434,6 +458,11 @@ public ResponseEntity> foo() { HttpStatus.OK); } + @RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE) + public String bar() { + return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; + } + } } From 2730a7ceef8e2a34f754cae4fea2e957cb69484a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 30 Sep 2015 19:28:43 +0100 Subject: [PATCH 008/898] Polishing --- .../headers/AbstractHeadersSnippet.java | 15 ++++-------- ...kMvcRestDocumentationIntegrationTests.java | 24 ------------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java index 5c3099b9d..f5e2c1269 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -75,18 +75,13 @@ protected Map createModel(Operation operation) { private void validateHeaderDocumentation(Operation operation) { List missingHeaders = findMissingHeaders(operation); - if (!missingHeaders.isEmpty()) { - String message = ""; - if (!missingHeaders.isEmpty()) { - List names = new ArrayList(); - for (HeaderDescriptor headerDescriptor : missingHeaders) { - names.add(headerDescriptor.getName()); - } - message += "Headers with the following names were not found in the " - + this.type + ": " + names; + List names = new ArrayList(); + for (HeaderDescriptor headerDescriptor : missingHeaders) { + names.add(headerDescriptor.getName()); } - throw new SnippetException(message); + throw new SnippetException("Headers with the following names were not found" + + " in the " + this.type + ": " + names); } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 8c5be89e5..546a17e75 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -400,30 +400,6 @@ public void customContextPath() throws Exception { "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } - @Test - public void stackOverflowQuestion() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); - - mockMvc.perform(get("/company/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document( - "company", - responseFields( - fieldWithPath("companyName").description( - "The name of the company"), - fieldWithPath("employee").description( - "An array of the company's employees")))) - .andDo(document( - "employee", - responseFields( - fieldWithPath("companyName").ignored(), - fieldWithPath("employee[].name").description( - "The name of the employee"), - fieldWithPath("employee[].age").description( - "The age of the employee")))); - } - private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { assertTrue(new File(directory, snippet).isFile()); From 723b7a284f1c4cecffe484f546e3b33a3c44a29b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 5 Oct 2015 10:22:55 +0100 Subject: [PATCH 009/898] Use default location for .adoc source in Gradle-based sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously both samples supported Maven and Gradle. Due to the different default locations between the two build systems, the Gradle-based sample was configured to use Maven’s default location for its .adoc files. The samples have now been updated so that one sample uses Maven and the other uses Gradle. However, when this change was made, the customization of the Gradle-based sample’s .adoc file location wasn’t removed. This commit updates the Gradle-based sample to use the default location, src/docs/asciidoc, for its .adoc source files. Closes gh-145 --- samples/rest-notes-spring-hateoas/build.gradle | 1 - .../src/{main => docs}/asciidoc/api-guide.adoc | 0 .../src/{main => docs}/asciidoc/getting-started-guide.adoc | 0 3 files changed, 1 deletion(-) rename samples/rest-notes-spring-hateoas/src/{main => docs}/asciidoc/api-guide.adoc (100%) rename samples/rest-notes-spring-hateoas/src/{main => docs}/asciidoc/getting-started-guide.adoc (100%) diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index a099b066e..f6c8ca4a3 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -48,7 +48,6 @@ test { } asciidoctor { - sourceDir 'src/main/asciidoc' // Align with Maven's default location attributes 'snippets': snippetsDir inputs.dir snippetsDir dependsOn test diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-hateoas/src/docs/asciidoc/api-guide.adoc similarity index 100% rename from samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.adoc rename to samples/rest-notes-spring-hateoas/src/docs/asciidoc/api-guide.adoc diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-hateoas/src/docs/asciidoc/getting-started-guide.adoc similarity index 100% rename from samples/rest-notes-spring-hateoas/src/main/asciidoc/getting-started-guide.adoc rename to samples/rest-notes-spring-hateoas/src/docs/asciidoc/getting-started-guide.adoc From a693d12ce0b3efa2d4ef666753c0c44537b29ae5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 16:29:23 +0100 Subject: [PATCH 010/898] Polish the documentation --- docs/src/docs/asciidoc/contributing.adoc | 6 +- .../docs/asciidoc/documenting-your-api.adoc | 74 ++++++++++++------- docs/src/docs/asciidoc/getting-started.adoc | 15 ++-- docs/src/docs/asciidoc/index.adoc | 6 +- docs/src/docs/asciidoc/introduction.adoc | 4 +- 5 files changed, 62 insertions(+), 43 deletions(-) diff --git a/docs/src/docs/asciidoc/contributing.adoc b/docs/src/docs/asciidoc/contributing.adoc index 660c3c09e..ce726ed63 100644 --- a/docs/src/docs/asciidoc/contributing.adoc +++ b/docs/src/docs/asciidoc/contributing.adoc @@ -19,7 +19,7 @@ Spring REST Docs users by answering questions. === Bugs If you believe you have found a bug, please take a moment to search the -{github}/issues?is%3Aissue[existing issues]. If no one else has reported the problem, +{github}/issues?q=is%3Aissue[existing issues]. If no one else has reported the problem, please {github}/issues/new[open a new issue] that describes the problem in detail and, ideally, includes a test that reproduces it. @@ -30,7 +30,7 @@ ideally, includes a test that reproduces it. If you'd like an enhancement to be made to Spring REST Docs, pull requests are most welcome. The source code is on {github}[GitHub]. You may want to search the -{github}/issues?is%3Aissue[existing issues] and {github}/pulls?q=is%3Apr[pull requests] to -see if the enhancement is already being worked on. You may also want to +{github}/issues?q=is%3Aissue[existing issues] and {github}/pulls?q=is%3Apr[pull requests] +to see if the enhancement is already being worked on. You may also want to {github}/issues/new[open a new issue] to discuss a possible enhancement before work on it begins. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index af979cb05..c7a184cb5 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -15,10 +15,11 @@ https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API: ---- include::{examples-dir}/com/example/Hypermedia.java[tag=links] ---- -<1> Produce a snippet describing the response's links. Uses the static `links` method on -`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. +<1> Configure Spring REST docs to 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`. + `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. <3> Expect a link whose rel is `bravo`. The result is a snippet named `links.adoc` that contains a table describing the resource's @@ -28,6 +29,9 @@ When documenting links, the test will fail if an undocumented link is found in t response. Similarly, the test will also fail if a documented link is not found in the response and the link has not been marked as optional. +If you do not want to document a link, you can mark it as ignored. This will prevent it +from appearing in the generated snippet while avoiding the failure described above. + [[documenting-your-api-hypermedia-link-formats]] @@ -65,11 +69,11 @@ provided. For example: ---- include::{examples-dir}/com/example/Payload.java[tags=response] ---- -<1> Use `responseFields` to describe the expected fields in the response payload. -To document a request `requestFields` can be used. Both are static methods on -`org.springframework.restdocs.payload.PayloadDocumentation`. +<1> Configure Spring REST docs to produce a snippet describing the fields in the response + payload. To document a request `requestFields` can be used. Both are static methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. <2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on -`org.springframework.restdocs.payload.PayloadDocumentation`. + `org.springframework.restdocs.payload.PayloadDocumentation`. <3> Expect a field with the path `contact.email`. The result is a snippet that contains a table describing the fields. For requests this @@ -82,6 +86,9 @@ payload and the field has not been marked as optional. For payloads with a hiera structure, documenting a field is sufficient for all of its descendants to also be treated as having been documented. +If you do not want to document a field, you can mark it as ignored. This will prevent it +from appearing in the generated snippet while avoiding the failure described above. + TIP: By default, Spring REST Docs will assume that the payload you are documenting is JSON. If you want to document an XML payload the content type of the request or response must be compatible with `application/xml`. @@ -195,7 +202,8 @@ examining the payload. Seven different types are supported: |=== The type can also be set explicitly using the `type(Object)` method on -`FieldDescriptor`. Typically, one of the values enumerated by `JsonFieldType` will be +`FieldDescriptor`. The result of the supplied ``Object``'s `toString` method will be used +in the documentation. Typically, one of the values enumerated by `JsonFieldType` will be used: [source,java,indent=0] @@ -220,7 +228,7 @@ XML field paths are described using XPath. `/` is used to descend into a child n When documenting an XML payload, you must provide a type for the field using the `type(Object)` method on `FieldDescriptor`. The result of the supplied type's `toString` -method will be included in the documentation. +method will be used in the documentation. @@ -236,10 +244,11 @@ include::{examples-dir}/com/example/RequestParameters.java[tags=request-paramete ---- <1> Perform a `GET` request with two parameters, `page` and `per_page` in the query string. -<2> Produce a snippet describing the request's parameters. Uses the static -`requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. + Uses the static `requestParameters` method on + `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the `page` parameter. Uses the static `parameterWithName` method on -`org.springframework.restdocs.request.RequestDocumentation`. + `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the `per_page` parameter. Request parameters can also be included as form data in the body of a POST request: @@ -257,6 +266,10 @@ When documenting request parameters, the test will fail if an undocumented reque parameter is used in the request. Similarly, the test will also fail if a documented request parameter is not found in the request. +If you do not want to document a request parameter, you can mark it as ignored. This will +prevent it from appearing in the generated snippet while avoiding the failure described +above. + [[documenting-your-api-path-parameters]] @@ -269,22 +282,27 @@ A request's path parameters can be documented using `pathParameters`. For exampl include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] ---- <1> Perform a `GET` request with two path parameters, `latitude` and `longitude`. -<2> Produce a snippet describing the request's path parameters. Uses the static -`pathParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's path + parameters. Uses the static `pathParameters` method on + `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the parameter named `latitude`. Uses the static `parameterWithName` method on -`org.springframework.restdocs.request.RequestDocumentation`. + `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the parameter named `longitude`. The result is a snippet named `path-parameters.adoc` that contains a table describing the path parameters that are supported by the resource. +TIP: To make the path parameters available for documentation, the request must be +built using one of the methods on `RestDocumentationRequestBuilders` rather than +`MockMvcRequestBuilders`. + When documenting path parameters, the test will fail if an undocumented path parameter is used in the request. Similarly, the test will also fail if a documented path parameter is not found in the request. -TIP: To make the path parameters available for documentation, the request must be -built using one of the methods on `RestDocumentationRequestBuilders` rather than -`MockMvcRequestBuilders`. +If you do not want to document a path parameter, you can mark it as ignored. This will +prevent it from appearing in the generated snippet while avoiding the failure described +above. @@ -299,12 +317,13 @@ The headers in a request or response can be documented using `requestHeaders` an include::{examples-dir}/com/example/HttpHeaders.java[tags=headers] ---- <1> Perform a `GET` request with an `Authorization` header that uses basic authentication -<2> Produce a snippet describing the request's headers. Uses the static `requestHeaders` -method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<2> Configure Spring REST Docs to produce a snippet describing the request's headers. + Uses the static `requestHeaders` method on + `org.springframework.restdocs.headers.HeaderDocumentation`. <3> Document the `Authorization` header. Uses the static `headerWithName` method on -`org.springframework.restdocs.headers.HeaderDocumentation. + `org.springframework.restdocs.headers.HeaderDocumentation. <4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` -method on `org.springframework.restdocs.headers.HeaderDocumentation`. + method on `org.springframework.restdocs.headers.HeaderDocumentation`. The result is a snippet named `request-headers.adoc` and a snippet named `response-headers.adoc`. Each contains a table describing the headers. @@ -328,8 +347,8 @@ include::{examples-dir}/com/example/Constraints.java[tags=constraints] <2> Get the descriptions of the name property's constraints. This list will contain two descriptions; one for the `NotNull` constraint and one for the `Size` constraint. -The `ApiDocumentation` class in the Spring HATEOAS sample shows this functionality in -action. +The {samples}/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java[`ApiDocumentation`] +class in the Spring HATEOAS sample shows this functionality in action. @@ -457,7 +476,7 @@ are supported: |=== For example, `document("{class-name}/{method-name}")` in a test method named -`creatingANote` on the test class `GettingStartedDocumentaiton`, will write +`creatingANote` on the test class `GettingStartedDocumentation`, will write snippets into a directory named `getting-started-documentation/creating-a-note`. A parameterized output directory is particularly useful in combination with Spring MVC @@ -485,7 +504,7 @@ sample applications to see this functionality in action. ==== Customizing the generated snippets Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the generated snippets. -{source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates[Default +{source}/spring-restdocs/src/main/resources/org/springframework/restdocs/templates[Default templates] are provided for each of the snippets that Spring REST Docs can produce. To customize a snippet's content, you can provide your own template. @@ -501,8 +520,7 @@ override the template for the `curl-request.adoc` snippet, create a template nam There are two ways to provide extra information for inclusion in a generated snippet: -. Use the `attributes` method on a field, link or parameter descriptor to add one or more - attributes to an individual descriptor. +. Use the `attributes` method on a descriptor to add one or more attributes to it. . Pass in some attributes when calling `curlRequest`, `httpRequest`, `httpResponse`, etc. Such attributes will be associated with the snippet as a whole. diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index ed25c8abf..136d98069 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -15,9 +15,9 @@ http://projects.spring.io/spring-hateoas/[Spring HATEOAS] and http://projects.spring.io/spring-data-rest/[Spring Data REST]. Both samples use Spring REST Docs to produce a detailed API guide and a getting started walkthrough. -In each sample the source for the documentation can be found in `src/main/asciidoc`. -`api-guide.adoc` produces an API guide for the service. `getting-started-guide.adoc` -produces a getting started guide that provides an introductory walkthrough. +Each sample contains a file named `api-guide.adoc` that produces an API guide for the +service, and a file named `getting-started-guide.adoc` that produces a getting started +guide that provides an introductory walkthrough. The code that produces the generated snippets can be found in `src/test/java`. `ApiDocumentation.java` produces the snippets for the API guide. @@ -35,7 +35,7 @@ The first step in using Spring REST Docs is to configure your project's build. [[getting-started-build-configuration-gradle]] ==== Gradle build configuration -The {samples}rest-notes-spring-hateoas[Spring HATEOAS sample] contains a `build.gradle` +The {samples}/rest-notes-spring-hateoas[Spring HATEOAS sample] contains a `build.gradle` file that you may wish to use as a reference. The key parts of the configuration are described below. @@ -99,7 +99,7 @@ directory: [[getting-started-build-configuration-maven]] ==== Maven build configuration -The {samples}rest-notes-spring-data-rest[Spring Data REST sample] contains a `pom.xml` +The {samples}/rest-notes-spring-data-rest[Spring Data REST sample] contains a `pom.xml` file that you may wish to use as a reference. The key parts of the configuration are described below. @@ -210,7 +210,8 @@ be included in the project's jar: ---- <1> The existing declaration for the Asciidoctor plugin. <2> The resource plugin must be declared after the Asciidoctor plugin as they are bound -to the same phase and the resource plugin must run after the Asciidoctor plugin. +to the same phase (`prepare-package`) and the resource plugin must run after the +Asciidoctor plugin. @@ -218,7 +219,7 @@ to the same phase and the resource plugin must run after the Asciidoctor plugin. === Generating documentation snippets Spring REST Docs uses {spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test framework] to make requests to the service that you are documenting. It then produces -documentation snippets for the result's request and response. +documentation snippets for request and resulting response. diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index d4821b024..86c27c47f 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -9,9 +9,9 @@ Andy Wilkinson :examples-dir: ../../test/java :github: https://github.com/spring-projects/spring-restdocs -:source: {github}/tree/{branch-or-tag}/ -:samples: {source}/samples -:templates: {source}/spring-restdocs/src/main/resources/org/springframework/restdocs/templates +:source: {github}/tree/{branch-or-tag} +:samples: {source}samples +:templates: {source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates :spring-boot-docs: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle :spring-framework-docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index f85bedf18..9590ac358 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -7,10 +7,10 @@ services that is accurate and readable. Writing high-quality documentation is difficult. One way to ease that difficulty is to use tools that are well-suited to the job. To this end, Spring REST Docs uses http://asciidoctor.org[Asciidoctor]. Asciidoctor processes plain text and produces -HTML styled and layed out to suit your needs. +HTML, styled and layed out to suit your needs. Spring REST Docs makes use of snippets produced by tests written with -{spring-framework-docs}#spring-mvc-test-framework[Spring MVC Test]. This test-driven +{spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test]. This test-driven approach helps to guarantee the accuracy of your service's documentation. If a snippet is incorrect the test that produces it will fail. From c97efcce2ebdd4303a83431c619f37d6090281a0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 16:41:34 +0100 Subject: [PATCH 011/898] Use the Maven Wrapper in the Spring Data REST-based sample Closes gh-146 --- .../build/SampleBuildConfigurer.groovy | 9 +- .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 49519 bytes .../.mvn/wrapper/maven-wrapper.properties | 1 + samples/rest-notes-spring-data-rest/mvnw | 234 ++++++++++++++++++ samples/rest-notes-spring-data-rest/mvnw.cmd | 145 +++++++++++ .../main/asciidoc/getting-started-guide.adoc | 2 +- 6 files changed, 386 insertions(+), 5 deletions(-) create mode 100644 samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar create mode 100644 samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties create mode 100755 samples/rest-notes-spring-data-rest/mvnw create mode 100644 samples/rest-notes-spring-data-rest/mvnw.cmd diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 96db02084..7538a6fef 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,10 +61,7 @@ public class SampleBuildConfigurer { mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - String suffix = File.separatorChar == '/' ? '' : '.bat' - mavenBuild.commandLine = [System.env.MAVEN_HOME ? - "${System.env.MAVEN_HOME}/bin/mvn${suffix}" : "mvn${suffix}", - 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? "mvnw.bat" : "./mvnw", 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { @@ -76,6 +73,10 @@ public class SampleBuildConfigurer { return mavenBuild } + private boolean isWindows() { + return File.separatorChar == '\\' + } + private Task createGradleBuild(Project project, Object... dependencies) { Task gradleBuild = project.tasks.create("${name}Gradle", GradleBuild) gradleBuild.description = "Builds the ${name} sample with Gradle" diff --git a/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar b/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c6feb8bb6f76f2553e266ff8bf8867105154237e GIT binary patch literal 49519 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%0A$pwhjXzyA0ZzErH{1@M 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 literal 0 HcmV?d00001 diff --git a/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties b/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..eb9194764 --- /dev/null +++ b/samples/rest-notes-spring-data-rest/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.3/apache-maven-3.3.3-bin.zip \ No newline at end of file diff --git a/samples/rest-notes-spring-data-rest/mvnw b/samples/rest-notes-spring-data-rest/mvnw new file mode 100755 index 000000000..fc7efd17d --- /dev/null +++ b/samples/rest-notes-spring-data-rest/mvnw @@ -0,0 +1,234 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + wdir=$(cd "$wdir/.."; pwd) + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CMD_LINE_ARGS + diff --git a/samples/rest-notes-spring-data-rest/mvnw.cmd b/samples/rest-notes-spring-data-rest/mvnw.cmd new file mode 100644 index 000000000..001048081 --- /dev/null +++ b/samples/rest-notes-spring-data-rest/mvnw.cmd @@ -0,0 +1,145 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %* + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR=""%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc index 420681011..df16161ad 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/getting-started-guide.adoc @@ -35,7 +35,7 @@ Once the clone is complete, you're ready to get the service up and running: [source,bash] ---- $ cd samples/rest-notes-spring-data-rest -$ mvn clean package +$ ./mvnw clean package $ java -jar target/*.jar ---- From f07f23ec85e7654346087b11cdd0f8268c791d5e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 16:49:07 +0100 Subject: [PATCH 012/898] Update versions of various dependencies --- build.gradle | 10 +++++----- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index ac4c5afa3..35db2d4e7 100644 --- a/build.gradle +++ b/build.gradle @@ -56,18 +56,18 @@ subprojects { mavenBom "org.springframework:spring-framework-bom:$springVersion" } dependencies { - dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6' - dependency 'com.samskivert:jmustache:1.10' + dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6.1' + dependency 'com.samskivert:jmustache:1.11' dependency 'commons-codec:commons-codec:1.10' dependency 'javax.servlet:javax.servlet-api:3.1.0' dependency 'javax.validation:validation-api:1.1.0.Final' dependency 'junit:junit:4.12' dependency 'org.hamcrest:hamcrest-core:1.3' dependency 'org.hamcrest:hamcrest-library:1.3' - dependency 'org.hibernate:hibernate-validator:5.2.1.Final' + dependency 'org.hibernate:hibernate-validator:5.2.2.Final' dependency 'org.mockito:mockito-core:1.10.19' - dependency 'org.springframework.hateoas:spring-hateoas:0.17.0.RELEASE' - dependency 'org.jacoco:org.jacoco.agent:0.7.2.201409121644' + dependency 'org.springframework.hateoas:spring-hateoas:0.19.0.RELEASE' + dependency 'org.jacoco:org.jacoco.agent:0.7.5.201505241946' } } diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index ad8688f23..020030dc4 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.2.5.RELEASE + 1.2.6.RELEASE diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index f6c8ca4a3..22a2e03e7 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.6.RELEASE' } } @@ -36,7 +36,7 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-hateoas' runtime 'com.h2database:h2' - runtime 'org.atteo:evo-inflector:1.2' + runtime 'org.atteo:evo-inflector:1.2.1' testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' From 9a362abe7796207eef9129092cf239500c85ab94 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 17:00:33 +0100 Subject: [PATCH 013/898] Update reference to mvnw.bat when building samples on Windows --- .../springframework/restdocs/build/SampleBuildConfigurer.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 7538a6fef..e05478d6d 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,7 +61,7 @@ public class SampleBuildConfigurer { mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? "mvnw.bat" : "./mvnw", 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? ".\\mvnw.bat" : "./mvnw", 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { From 75e965c7823cdf5da9db4c1997342a63a10d1772 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 17:15:47 +0100 Subject: [PATCH 014/898] Correct the name of the mvnw script on Windows --- .../springframework/restdocs/build/SampleBuildConfigurer.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index e05478d6d..ea9672201 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,7 +61,7 @@ public class SampleBuildConfigurer { mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? ".\\mvnw.bat" : "./mvnw", 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? "mvnw.cmd" : "./mvnw", 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { From 6443ed177e22a59084a016b1e5809abdf916c3a5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 6 Oct 2015 17:46:27 +0100 Subject: [PATCH 015/898] Fix building Maven sample on Windows --- .../springframework/restdocs/build/SampleBuildConfigurer.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index ea9672201..db94081e1 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,7 +61,7 @@ public class SampleBuildConfigurer { mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? "mvnw.cmd" : "./mvnw", 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? "${new File(this.workingDir).absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { From 0213c9e6fdb2ea418d8ea05b8b8eb866cc751408 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 7 Oct 2015 09:47:21 +0100 Subject: [PATCH 016/898] Add the Spring IO plugin to the build --- build.gradle | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/build.gradle b/build.gradle index 35db2d4e7..5fa1b286f 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ buildscript { dependencies { classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' + classpath 'io.spring.gradle:spring-io-plugin:0.0.4.RELEASE' } } @@ -51,6 +52,22 @@ subprojects { sourceCompatibility = 1.7 targetCompatibility = 1.7 + if (project.hasProperty('platformVersion')) { + apply plugin: 'spring-io' + + repositories { + maven { url "https://repo.spring.io/libs-snapshot" } + } + + dependencyManagement { + springIoTestRuntime { + imports { + mavenBom "io.spring.platform:platform-bom:${platformVersion}" + } + } + } + } + dependencyManagement { imports { mavenBom "org.springframework:spring-framework-bom:$springVersion" From 6debc593e567d74ca1d6ed9bc3ea90b102a614d8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 7 Oct 2015 10:38:50 +0100 Subject: [PATCH 017/898] Don't apply to Spring IO Plugin to the docs project --- build.gradle | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index 5fa1b286f..593cde259 100644 --- a/build.gradle +++ b/build.gradle @@ -52,22 +52,6 @@ subprojects { sourceCompatibility = 1.7 targetCompatibility = 1.7 - if (project.hasProperty('platformVersion')) { - apply plugin: 'spring-io' - - repositories { - maven { url "https://repo.spring.io/libs-snapshot" } - } - - dependencyManagement { - springIoTestRuntime { - imports { - mavenBom "io.spring.platform:platform-bom:${platformVersion}" - } - } - } - } - dependencyManagement { imports { mavenBom "org.springframework:spring-framework-bom:$springVersion" @@ -110,6 +94,22 @@ configure(subprojects - project(":docs")) { subproject -> apply plugin: 'checkstyle' apply from: "${rootProject.projectDir}/gradle/publish-maven.gradle" + if (project.hasProperty('platformVersion')) { + apply plugin: 'spring-io' + + repositories { + maven { url "https://repo.spring.io/libs-snapshot" } + } + + dependencyManagement { + springIoTestRuntime { + imports { + mavenBom "io.spring.platform:platform-bom:${platformVersion}" + } + } + } + } + checkstyle { configFile = rootProject.file('config/checkstyle/checkstyle.xml') configProperties = [ 'checkstyle.config.dir' : rootProject.file('config/checkstyle') ] From cfcc43c482d18846f8dc7c21d57a51105b315fbb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 7 Oct 2015 11:48:09 +0100 Subject: [PATCH 018/898] Ensure that generated poms contain and --- spring-restdocs-core/build.gradle | 2 ++ spring-restdocs-mockmvc/build.gradle | 2 ++ 2 files changed, 4 insertions(+) diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 2de27164d..2ba4b30bb 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -1,3 +1,5 @@ +description = 'Spring REST Docs Core' + configurations { jarjar jmustache diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index 597741053..ba3ab171b 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -1,3 +1,5 @@ +description = 'Spring REST Docs MockMvc' + dependencies { compile 'org.springframework:spring-test' compile project(':spring-restdocs-core') From 260ffeebbaf5ba9d70192335d8610b35c1c38d41 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Wed, 7 Oct 2015 05:48:20 -0700 Subject: [PATCH 019/898] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 83e39c4af..59241404f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.0.0.BUILD-SNAPSHOT +version=1.0.1.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From 69f6b40e28faf86adf5025a91b1f69b18c7a41d3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 9 Oct 2015 09:41:52 +0100 Subject: [PATCH 020/898] Correct the links in the documentation to the samples on GitHub Closes gh-147 --- docs/src/docs/asciidoc/index.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 86c27c47f..17cde86ea 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -10,7 +10,7 @@ Andy Wilkinson :examples-dir: ../../test/java :github: https://github.com/spring-projects/spring-restdocs :source: {github}/tree/{branch-or-tag} -:samples: {source}samples +:samples: {source}/samples :templates: {source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates :spring-boot-docs: http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle :spring-framework-docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle From 697275f347693ff47e682ca2a4cabf8816b59cbc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Oct 2015 17:51:17 +0100 Subject: [PATCH 021/898] Begin development of 1.1.0 --- gradle.properties | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 59241404f..4797e2a66 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.0.1.BUILD-SNAPSHOT +version=1.1.0.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 020030dc4..274f9569b 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.0.0.BUILD-SNAPSHOT + 1.1.0.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 22a2e03e7..414a9081b 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -28,7 +28,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.0.0.BUILD-SNAPSHOT' + springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } dependencies { From 9a3e4c5f701040a1eccf2786fa278caab860ab9a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 22 Oct 2015 10:03:57 +0100 Subject: [PATCH 022/898] Update the samples to build against 1.0.1 snapshots --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 020030dc4..aa6384b6c 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.0.0.BUILD-SNAPSHOT + 1.0.1.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 22a2e03e7..8dfb61e8a 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -28,7 +28,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.0.0.BUILD-SNAPSHOT' + springRestdocsVersion = '1.0.1.BUILD-SNAPSHOT' } dependencies { From 75085477b69fb5b936b6c9eefe80730fe913d30f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 22 Oct 2015 09:42:26 +0100 Subject: [PATCH 023/898] Suppress unwanted output to System.err when pretty printing content Previously, when the XML pretty printer attempted to pretty print the content of a request or response it would output an error to System.err if the content was not valid XML. While, benign, this output was distracting. This commit updates the XML pretty printer to suppress its output to System.err by configuring it with an ErrorHandler and an ErrorListener that swallow any errors of which they are notified. Closes gh-153 --- .../PrettyPrintingContentModifier.java | 79 ++++++++- .../PrettyPrintingContentModifierTests.java | 8 + .../restdocs/test/OutputCapture.java | 154 ++++++++++++++++++ 3 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 49af2bf46..16c9471ac 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -23,13 +23,23 @@ import java.util.Collections; import java.util.List; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.ErrorListener; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; -import javax.xml.transform.stream.StreamSource; import org.springframework.http.MediaType; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.XMLReader; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @@ -47,12 +57,14 @@ public class PrettyPrintingContentModifier implements ContentModifier { @Override public byte[] modifyContent(byte[] originalContent, MediaType contentType) { - for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { - try { - return prettyPrinter.prettyPrint(originalContent).getBytes(); - } - catch (Exception ex) { - // Continue + if (originalContent.length > 0) { + for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { + try { + return prettyPrinter.prettyPrint(originalContent).getBytes(); + } + catch (Exception ex) { + // Continue + } } } return originalContent; @@ -74,10 +86,61 @@ public String prettyPrint(byte[] original) throws Exception { "4"); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes"); StringWriter transformed = new StringWriter(); - transformer.transform(new StreamSource(new ByteArrayInputStream(original)), + transformer.setErrorListener(new SilentErrorListener()); + transformer.transform(createSaxSource(original), new StreamResult(transformed)); return transformed.toString(); } + + private SAXSource createSaxSource(byte[] original) + throws ParserConfigurationException, SAXException { + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + SAXParser parser = parserFactory.newSAXParser(); + XMLReader xmlReader = parser.getXMLReader(); + xmlReader.setErrorHandler(new SilentErrorHandler()); + SAXSource xmlInput = new SAXSource(xmlReader, new InputSource( + new ByteArrayInputStream(original))); + return xmlInput; + } + + private static final class SilentErrorListener implements ErrorListener { + + @Override + public void warning(TransformerException exception) + throws TransformerException { + + } + + @Override + public void error(TransformerException exception) throws TransformerException { + + } + + @Override + public void fatalError(TransformerException exception) + throws TransformerException { + + } + + } + + private static final class SilentErrorHandler implements ErrorHandler { + + @Override + public void warning(SAXParseException exception) throws SAXException { + + } + + @Override + public void error(SAXParseException exception) throws SAXException { + + } + + @Override + public void fatalError(SAXParseException exception) throws SAXException { + + } + } } private static final class JsonPrettyPrinter implements PrettyPrinter { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index 85e5edf9a..a06c94006 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -16,9 +16,12 @@ package org.springframework.restdocs.operation.preprocess; +import org.junit.Rule; import org.junit.Test; +import org.springframework.restdocs.test.OutputCapture; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.isEmptyString; import static org.junit.Assert.assertThat; /** @@ -29,6 +32,9 @@ */ public class PrettyPrintingContentModifierTests { + @Rule + public OutputCapture outputCapture = new OutputCapture(); + @Test public void prettyPrintJson() throws Exception { assertThat(new PrettyPrintingContentModifier().modifyContent( @@ -56,7 +62,9 @@ public void empytContentIsHandledGracefully() throws Exception { @Test public void nonJsonAndNonXmlContentIsHandledGracefully() throws Exception { String content = "abcdefg"; + this.outputCapture.expect(isEmptyString()); assertThat(new PrettyPrintingContentModifier().modifyContent(content.getBytes(), null), equalTo(content.getBytes())); + } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java new file mode 100644 index 000000000..4aab96685 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-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.test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import org.hamcrest.Matcher; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import static org.hamcrest.Matchers.allOf; +import static org.junit.Assert.assertThat; + +/** + * JUnit {@code @Rule} to capture output from System.out and System.err. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +public class OutputCapture implements TestRule { + + private CaptureOutputStream captureOut; + + private CaptureOutputStream captureErr; + + private ByteArrayOutputStream capturedOutput; + + private List> matchers = new ArrayList>(); + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + captureOutput(); + try { + base.evaluate(); + } + finally { + try { + if (!OutputCapture.this.matchers.isEmpty()) { + assertThat(getOutputAsString(), + allOf(OutputCapture.this.matchers)); + } + } + finally { + releaseOutput(); + } + } + } + }; + } + + private void captureOutput() { + this.capturedOutput = new ByteArrayOutputStream(); + this.captureOut = new CaptureOutputStream(System.out, this.capturedOutput); + this.captureErr = new CaptureOutputStream(System.err, this.capturedOutput); + System.setOut(new PrintStream(this.captureOut)); + System.setErr(new PrintStream(this.captureErr)); + } + + private String getOutputAsString() { + flush(); + return this.capturedOutput.toString(); + } + + private void releaseOutput() { + System.setOut(this.captureOut.getOriginal()); + System.setErr(this.captureErr.getOriginal()); + this.capturedOutput = null; + } + + private void flush() { + try { + this.captureOut.flush(); + this.captureErr.flush(); + } + catch (IOException ex) { + // ignore + } + } + + /** + * Verify that the output is matched by the supplied {@code matcher}. Verification is + * performed after the test method has executed. + * + * @param matcher the matcher + */ + public final void expect(Matcher matcher) { + this.matchers.add(matcher); + } + + private static final class CaptureOutputStream extends OutputStream { + + private final PrintStream original; + + private final OutputStream copy; + + private CaptureOutputStream(PrintStream original, OutputStream copy) { + this.original = original; + this.copy = copy; + } + + @Override + public void write(int b) throws IOException { + this.copy.write(b); + this.original.write(b); + this.original.flush(); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + this.copy.write(b, off, len); + this.original.write(b, off, len); + } + + private PrintStream getOriginal() { + return this.original; + } + + @Override + public void flush() throws IOException { + this.copy.flush(); + this.original.flush(); + } + + } + +} From 58b992d99eaf277da7671e53c0a7fd3a65bd9534 Mon Sep 17 00:00:00 2001 From: cschaetzlein Date: Mon, 16 Nov 2015 16:15:04 +0100 Subject: [PATCH 024/898] Allow individual attributes in an XML element to be documented Documenting an XML attribute in the XML payload leads to a NullPointerException since no parent nodes exists for an XML attribute. Rather than always trying to remove a node from its parent, this commit changes the logic to apply special treament to nodes that are attributes and remove the attribute from its owning element instead. Closes gh-167 --- .../restdocs/payload/XmlContentHandler.java | 11 ++++++++++- .../payload/ResponseFieldsSnippetTests.java | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index a971f049c..89b0aecf2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -34,6 +34,7 @@ import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; +import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -115,7 +116,15 @@ public String getUndocumentedContent(List fieldDescriptors) { } for (int i = 0; i < matchingNodes.getLength(); i++) { Node node = matchingNodes.item(i); - node.getParentNode().removeChild(node); + + if (node.getNodeType() == Node.ATTRIBUTE_NODE) { + Attr attr = (Attr) node; + attr.getOwnerElement().removeAttributeNode(attr); + } + else { + node.getParentNode().removeChild(node); + } + } } if (payload.getChildNodes().getLength() > 0) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 337e6e8c6..720483012 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -192,6 +192,21 @@ public void undocumentedXmlResponseField() throws IOException { .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } + + @Test + public void undocumentedXmlWithAttributeResponseField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("The following parts of the payload were not" + + " documented:\r\n\r\n 5\r\n\r\n")); + new ResponseFieldsSnippet(Arrays.asList(new FieldDescriptor("/a/b/@id").type("").description(""))) + .document(new OperationBuilder("undocumented-xml-response-field", + this.snippet.getOutputDirectory()) + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } @Test public void xmlResponseFieldWithNoType() throws IOException { From acb87b98b872caa5c591a6ac5e3930d93ff42229 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 16 Nov 2015 17:41:42 +0000 Subject: [PATCH 025/898] Improve testing of XML attribute handling Closes gh-166 --- .../restdocs/payload/XmlContentHandler.java | 1 - .../payload/ResponseFieldsSnippetTests.java | 72 ++++++++++++++++--- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 89b0aecf2..798013736 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -116,7 +116,6 @@ public String getUndocumentedContent(List fieldDescriptors) { } for (int i = 0; i < matchingNodes.getLength(); i++) { Node node = matchingNodes.item(i); - if (node.getNodeType() == Node.ATTRIBUTE_NODE) { Attr attr = (Attr) node; attr.getOwnerElement().removeAttributeNode(attr); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 720483012..ad6653d38 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -192,22 +192,78 @@ public void undocumentedXmlResponseField() throws IOException { .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } - + + @Test + public void xmlAttribute() throws IOException { + this.snippet.expectResponseFields("xml-attribute").withContents( + tableWithHeader("Path", "Type", "Description").row("a", "b", "one").row( + "a/@id", "c", "two")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("b"), fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("xml-attribute", this.snippet + .getOutputDirectory()) + .response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + @Test - public void undocumentedXmlWithAttributeResponseField() throws IOException { + public void missingXmlAttribute() throws IOException { this.thrown.expect(SnippetException.class); this.thrown - .expectMessage(equalTo("The following parts of the payload were not" - + " documented:\r\n\r\n 5\r\n\r\n")); - new ResponseFieldsSnippet(Arrays.asList(new FieldDescriptor("/a/b/@id").type("").description(""))) - .document(new OperationBuilder("undocumented-xml-response-field", - this.snippet.getOutputDirectory()) + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/@id]")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("b"), fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("missing-xml-attribute", this.snippet + .getOutputDirectory()) .response() - .content("5") + .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } + @Test + public void missingOptionalXmlAttribute() throws IOException { + this.snippet.expectResponseFields("missing-optional-xml-attribute").withContents( + tableWithHeader("Path", "Type", "Description").row("a", "b", "one").row( + "a/@id", "c", "two")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("b"), fieldWithPath("a/@id").description("two").type("c") + .optional())).document(new OperationBuilder( + "missing-optional-xml-attribute", this.snippet.getOutputDirectory()) + .response().content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void undocumentedAttributeDoesNotCauseFailure() throws IOException { + this.snippet.expectResponseFields("undocumented-attribute").withContents( + tableWithHeader("Path", "Type", "Description").row("a", "a", "one")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("a"))).document(new OperationBuilder("undocumented-attribute", + this.snippet.getOutputDirectory()).response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void documentedXmlAttributesAreRemoved() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo(String + .format("The following parts of the payload were not documented:" + + "%nbar%n"))); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one") + .type("a"))).document(new OperationBuilder( + "documented-attribute-is-removed", this.snippet.getOutputDirectory()) + .response().content("bar") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + @Test public void xmlResponseFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); From c559efb81b987d5ee24d6a8029312cea5201429e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 3 Dec 2015 16:02:16 +0000 Subject: [PATCH 026/898] Provide examples for JUnit rule configuration with both Maven and Gradle Closes gh-162 --- docs/src/docs/asciidoc/getting-started.adoc | 28 +++++++++++++++---- .../com/example/ExampleApplicationTests.java | 5 ++-- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 136d98069..6a652de67 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -227,17 +227,33 @@ documentation snippets for request and resulting response. ==== Setting up Spring MVC test The first step in generating documentation snippets is to declare a `public` -`RestDocumentation` field that's annotated as a JUnit `@Rule` and to provide an `@Before` -method that creates a `MockMvc` instance: +`RestDocumentation` field that's annotated as a JUnit `@Rule`. The `RestDocumentation` +rule is configured with the output directory into which generated snippets should be +written. This output directory should match the snippets directory that you have +configured in your `build.gradle` or `pom.xml` file. + +For Maven (`pom.xml` that will typically be `target/generated-snippets`: [source,java,indent=0] ---- -include::{examples-dir}/com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] +@Rule +public RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); +---- + +And for Gradle (`build.gradle`) it will typically be `build/generated-snippets`: + +[source,java,indent=0] ---- +@Rule +public RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); +---- + +Next, provide an `@Before` method that creates a `MockMvc` instance: -The `RestDocumentation` rule is configured with the output directory into which -generated snippets should be written. This output directory should match the snippets -directory that you have configured in your `build.gradle` or `pom.xml` file. +[source,java,indent=0] +---- +include::{examples-dir}/com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] +---- The `MockMvc` instance is configured using a `RestDocumentationMockMvcConfigurer`. An instance of this class can be obtained from the static `documentationConfiguration()` diff --git a/docs/src/test/java/com/example/ExampleApplicationTests.java b/docs/src/test/java/com/example/ExampleApplicationTests.java index d94622f5f..70fb8e188 100644 --- a/docs/src/test/java/com/example/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/ExampleApplicationTests.java @@ -28,10 +28,9 @@ public class ExampleApplicationTests { - // tag::mock-mvc-setup[] @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); - + public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); + // tag::mock-mvc-setup[] @Autowired private WebApplicationContext context; From 7a819b15b606dc60865d2c04a5ace1c94fa9f93a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 3 Dec 2015 17:39:00 +0000 Subject: [PATCH 027/898] Make it clear that JSON array responses can be documented Closes gh-163 --- .../src/docs/asciidoc/documenting-your-api.adoc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index c7a184cb5..840171c9f 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -167,6 +167,23 @@ The following paths are all present: |=== +A response that uses an array at its root can also be documented. The path `[]` will refer +to the entire array. You can then use bracket or dot notation to identify fields within +the array's entries. For example, `[].id` corresponds to the `id` field of every object +found in the following array: + +[source,json,indent=0] +---- + [ + { + "id":1 + }, + { + "id":2 + } + ] +---- + [[documenting-your-api-request-response-payloads-json-field-types]] From e8cf957f1506a2790259e3c9ad6004f69732d8c6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 3 Dec 2015 17:50:34 +0000 Subject: [PATCH 028/898] Update documentation to mention that you can write custom preprocessors Closes gh-156 --- .../customizing-requests-and-responses.adoc | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index f88a58795..37ca4e0a9 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -84,4 +84,15 @@ from the request or response. `replacePattern` on `Preprocessors` provides a general purpose mechanism for replacing content in a request or response. Any occurrences of a regular expression are -replaced. \ No newline at end of file +replaced. + +[[customizing-requests-and-responses-preprocessors-writing-your-own]] +==== Writing your own preprocessor + +If one of the built-in preprocessors does not meet your needs, you can write your own by +implementing the `OperationPreprocessor` interface. You can then use your custom +preprocessor in exactly the same way as any of the built-in preprocessors. + +If you only want to modify the content (body) of a request or response, consider +implementing the `ContentModifier` interface and using it with the built-in +`ContentModifyingOperationPreprocessor`. \ No newline at end of file From a3c3bef093fb77ccecd2568a7fa817a1df526f7b Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Thu, 29 Oct 2015 20:49:34 +0900 Subject: [PATCH 029/898] Fix the link in the documentation to the default snippet templates Closes gh-160 --- docs/src/docs/asciidoc/documenting-your-api.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 840171c9f..25fc52b15 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -521,7 +521,7 @@ sample applications to see this functionality in action. ==== Customizing the generated snippets Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the generated snippets. -{source}/spring-restdocs/src/main/resources/org/springframework/restdocs/templates[Default +{source}/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates[Default templates] are provided for each of the snippets that Spring REST Docs can produce. To customize a snippet's content, you can provide your own template. From 88d3e72ae94e38fb927cec000d39b78af0af5a9f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Dec 2015 10:48:56 +0000 Subject: [PATCH 030/898] Upgrade to Spring Framework 4.1.8.RELEASE --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 593cde259..8785ee21b 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ sonarRunner { } ext { - springVersion = '4.1.7.RELEASE' + springVersion = '4.1.8.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", From 939769db110505888338a124a4788cd5f5569d50 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Dec 2015 10:50:20 +0000 Subject: [PATCH 031/898] Upgrade the samples to Spring Boot 1.2.7.RELEASE --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index aa6384b6c..36aa3ae74 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.2.6.RELEASE + 1.2.7.RELEASE diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 8dfb61e8a..8e7bd07d8 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' } } From f6f4b77331d438b7f6b8b715c96265584f951299 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Dec 2015 10:52:19 +0000 Subject: [PATCH 032/898] Upgrade the samples to use the latest Asciidoctor plugins --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 36aa3ae74..223ebdfcb 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -73,7 +73,7 @@ org.asciidoctor asciidoctor-maven-plugin - 1.5.2 + 1.5.2.1 generate-docs diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 8e7bd07d8..1f05c5414 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -8,7 +8,7 @@ buildscript { } plugins { - id "org.asciidoctor.convert" version "1.5.2" + id "org.asciidoctor.convert" version "1.5.3" } apply plugin: 'java' From 437d2bcbce13396da6ec9f707afb43645a12fd41 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Dec 2015 21:15:44 +0000 Subject: [PATCH 033/898] Uniquely identify the 1.0.x branch in Sonar --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 8785ee21b..2e078797c 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ apply plugin: 'sonar-runner' sonarRunner { sonarProperties { + property 'sonar.branch', '1.0.x' property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" property 'sonar.java.coveragePlugin', 'jacoco' property 'sonar.links.ci', 'https://build.spring.io/browse/SRD' From 83e56f68abf7d9a1174151f89f84f4f0d3c3f3b2 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Thu, 10 Dec 2015 01:43:58 -0800 Subject: [PATCH 034/898] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 59241404f..a651766f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.0.1.BUILD-SNAPSHOT +version=1.0.2.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From 99a2e4f0e0fddf5576ebb510de9f3b6cea4b74af Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 10 Dec 2015 09:55:36 +0000 Subject: [PATCH 035/898] Update the samples to build against 1.0.2 snapshots --- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 223ebdfcb..6e057717d 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.0.1.BUILD-SNAPSHOT + 1.0.2.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 1f05c5414..665e88f9a 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -28,7 +28,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.0.1.BUILD-SNAPSHOT' + springRestdocsVersion = '1.0.2.BUILD-SNAPSHOT' } dependencies { From 194e62bde3c1888986e6ba2107c8c300eb89a17c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sun, 17 Jan 2016 20:23:27 +0000 Subject: [PATCH 036/898] =?UTF-8?q?Remove=20assumption=20that=20working=20?= =?UTF-8?q?directory=20will=20be=20the=20project=E2=80=99s=20root?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin for building the samples assumed that the current working directory would be the directory containing the project’s build.gradle. While this is generally true on the command line (it isn’t if -project-dir is used), it isn’t true in Gradle’s IDE integration. This was leading to an NPE as the sampleBuild task was being configured to depend on a null task as a result of not finding the sample’s pom.xml or build.grade file. This commit corrects the main build.gradle to configure each sample’s working directory relative to the root project’s directory, rather than relying on the working directory. It also improves SampleBuildConfigurer so that it will throw an exception if it fails to find a sample’s build.gradle or pom.xml rather than continuing and triggering an NPE inside Gradle. Closes gh-185 --- build.gradle | 4 ++-- .../build/SampleBuildConfigurer.groovy | 18 +++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 2e078797c..743c45bb5 100644 --- a/build.gradle +++ b/build.gradle @@ -156,11 +156,11 @@ samples { dependOn 'spring-restdocs-mockmvc:install' restNotesSpringHateoas { - workingDir 'samples/rest-notes-spring-hateoas' + workingDir "$projectDir/samples/rest-notes-spring-hateoas" } restNotesSpringDataRest { - workingDir 'samples/rest-notes-spring-data-rest' + workingDir "$projectDir/samples/rest-notes-spring-data-rest" } } diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index db94081e1..5ddd17ab1 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -38,17 +38,21 @@ public class SampleBuildConfigurer { } Task createTask(Project project, Object... dependencies) { + File sampleDir = new File(this.workingDir).absoluteFile Task verifyIncludes - if (new File(this.workingDir, 'build.gradle').isFile()) { + if (new File(sampleDir, 'build.gradle').isFile()) { Task gradleBuild = createGradleBuild(project, dependencies) - verifyIncludes = createVerifyIncludes(project, new File(this.workingDir, 'build/asciidoc')) + verifyIncludes = createVerifyIncludes(project, new File(sampleDir, 'build/asciidoc')) verifyIncludes.dependsOn gradleBuild } - if (new File(this.workingDir, 'pom.xml').isFile()) { - Task mavenBuild = createMavenBuild(project, dependencies) - verifyIncludes = createVerifyIncludes(project, new File(this.workingDir, 'target/generated-docs')) + else if (new File(sampleDir, 'pom.xml').isFile()) { + Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) + verifyIncludes = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) verifyIncludes.dependsOn(mavenBuild) } + else { + throw new IllegalStateException("No pom.xml or build.gradle was found in $sampleDir") + } Task sampleBuild = project.tasks.create name sampleBuild.description = "Builds the ${name} sample" sampleBuild.group = "Build" @@ -56,12 +60,12 @@ public class SampleBuildConfigurer { return sampleBuild } - private Task createMavenBuild(Project project, Object... dependencies) { + private Task createMavenBuild(Project project, File sampleDir, Object... dependencies) { Task mavenBuild = project.tasks.create("${name}Maven", Exec) mavenBuild.description = "Builds the ${name} sample with Maven" mavenBuild.group = "Build" mavenBuild.workingDir = this.workingDir - mavenBuild.commandLine = [isWindows() ? "${new File(this.workingDir).absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] + mavenBuild.commandLine = [isWindows() ? "${sampleDir.absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] mavenBuild.dependsOn dependencies mavenBuild.doFirst { From 82a5b96b69532d81e45dd9df4ebebb8528e6afb0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Jan 2016 13:28:19 +0000 Subject: [PATCH 037/898] Update the default copyright header for 2016 --- config/eclipse/org.eclipse.jdt.ui.prefs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/eclipse/org.eclipse.jdt.ui.prefs b/config/eclipse/org.eclipse.jdt.ui.prefs index c5e23c905..681d02ad0 100644 --- a/config/eclipse/org.eclipse.jdt.ui.prefs +++ b/config/eclipse/org.eclipse.jdt.ui.prefs @@ -70,7 +70,7 @@ org.eclipse.jdt.ui.keywordthis=false org.eclipse.jdt.ui.ondemandthreshold=9999 org.eclipse.jdt.ui.overrideannotation=true org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= +org.eclipse.jdt.ui.text.custom_code_templates= sp_cleanup.add_default_serial_version_id=true sp_cleanup.add_generated_serial_version_id=false sp_cleanup.add_missing_annotations=true From 08b24e0496d65f474c7dfd8961a4e292a41a4bee Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 19 Jan 2016 13:36:20 +0000 Subject: [PATCH 038/898] Remove redundant Eclipse metadata --- .../.settings/org.eclipse.jdt.ui.prefs | 125 ------------------ .../.settings/org.eclipse.jdt.ui.prefs | 125 ------------------ 2 files changed, 250 deletions(-) delete mode 100644 spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs delete mode 100644 spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs diff --git a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index c5e23c905..000000000 --- a/spring-restdocs-core/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,125 +0,0 @@ -cleanup.add_default_serial_version_id=true -cleanup.add_generated_serial_version_id=false -cleanup.add_missing_annotations=true -cleanup.add_missing_deprecated_annotations=true -cleanup.add_missing_methods=false -cleanup.add_missing_nls_tags=false -cleanup.add_missing_override_annotations=true -cleanup.add_missing_override_annotations_interface_methods=true -cleanup.add_serial_version_id=false -cleanup.always_use_blocks=true -cleanup.always_use_parentheses_in_expressions=false -cleanup.always_use_this_for_non_static_field_access=true -cleanup.always_use_this_for_non_static_method_access=false -cleanup.convert_functional_interfaces=false -cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=false -cleanup.format_source_code=true -cleanup.format_source_code_changes_only=false -cleanup.insert_inferred_type_arguments=false -cleanup.make_local_variable_final=false -cleanup.make_parameters_final=false -cleanup.make_private_fields_final=false -cleanup.make_type_abstract_if_missing_method=false -cleanup.make_variable_declarations_final=false -cleanup.never_use_blocks=false -cleanup.never_use_parentheses_in_expressions=true -cleanup.organize_imports=true -cleanup.qualify_static_field_accesses_with_declaring_class=false -cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -cleanup.qualify_static_member_accesses_with_declaring_class=true -cleanup.qualify_static_method_accesses_with_declaring_class=false -cleanup.remove_private_constructors=true -cleanup.remove_redundant_type_arguments=true -cleanup.remove_trailing_whitespaces=true -cleanup.remove_trailing_whitespaces_all=true -cleanup.remove_trailing_whitespaces_ignore_empty=false -cleanup.remove_unnecessary_casts=true -cleanup.remove_unnecessary_nls_tags=false -cleanup.remove_unused_imports=true -cleanup.remove_unused_local_variables=false -cleanup.remove_unused_private_fields=true -cleanup.remove_unused_private_members=false -cleanup.remove_unused_private_methods=true -cleanup.remove_unused_private_types=true -cleanup.sort_members=false -cleanup.sort_members_all=false -cleanup.use_anonymous_class_creation=false -cleanup.use_blocks=true -cleanup.use_blocks_only_for_return_and_throw=false -cleanup.use_lambda=true -cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=false -cleanup.use_this_for_non_static_field_access_only_if_necessary=false -cleanup.use_this_for_non_static_method_access=false -cleanup.use_this_for_non_static_method_access_only_if_necessary=true -cleanup.use_type_arguments=false -cleanup_profile=_Spring REST Docs Cleanup Conventions -cleanup_settings_version=2 -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Spring REST Docs Java Conventions -formatter_settings_version=12 -org.eclipse.jdt.ui.exception.name=e -org.eclipse.jdt.ui.gettersetter.use.is=false -org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=java;javax;org;com;;\#; -org.eclipse.jdt.ui.javadoc=true -org.eclipse.jdt.ui.keywordthis=false -org.eclipse.jdt.ui.ondemandthreshold=9999 -org.eclipse.jdt.ui.overrideannotation=true -org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=true -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_missing_override_annotations_interface_methods=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=true -sp_cleanup.always_use_this_for_non_static_field_access=true -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=true -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.make_local_variable_final=false -sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=false -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=false -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=true -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=true -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=true -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=true -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=true -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=true -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true diff --git a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs b/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs deleted file mode 100644 index c5e23c905..000000000 --- a/spring-restdocs-mockmvc/.settings/org.eclipse.jdt.ui.prefs +++ /dev/null @@ -1,125 +0,0 @@ -cleanup.add_default_serial_version_id=true -cleanup.add_generated_serial_version_id=false -cleanup.add_missing_annotations=true -cleanup.add_missing_deprecated_annotations=true -cleanup.add_missing_methods=false -cleanup.add_missing_nls_tags=false -cleanup.add_missing_override_annotations=true -cleanup.add_missing_override_annotations_interface_methods=true -cleanup.add_serial_version_id=false -cleanup.always_use_blocks=true -cleanup.always_use_parentheses_in_expressions=false -cleanup.always_use_this_for_non_static_field_access=true -cleanup.always_use_this_for_non_static_method_access=false -cleanup.convert_functional_interfaces=false -cleanup.convert_to_enhanced_for_loop=false -cleanup.correct_indentation=false -cleanup.format_source_code=true -cleanup.format_source_code_changes_only=false -cleanup.insert_inferred_type_arguments=false -cleanup.make_local_variable_final=false -cleanup.make_parameters_final=false -cleanup.make_private_fields_final=false -cleanup.make_type_abstract_if_missing_method=false -cleanup.make_variable_declarations_final=false -cleanup.never_use_blocks=false -cleanup.never_use_parentheses_in_expressions=true -cleanup.organize_imports=true -cleanup.qualify_static_field_accesses_with_declaring_class=false -cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -cleanup.qualify_static_member_accesses_with_declaring_class=true -cleanup.qualify_static_method_accesses_with_declaring_class=false -cleanup.remove_private_constructors=true -cleanup.remove_redundant_type_arguments=true -cleanup.remove_trailing_whitespaces=true -cleanup.remove_trailing_whitespaces_all=true -cleanup.remove_trailing_whitespaces_ignore_empty=false -cleanup.remove_unnecessary_casts=true -cleanup.remove_unnecessary_nls_tags=false -cleanup.remove_unused_imports=true -cleanup.remove_unused_local_variables=false -cleanup.remove_unused_private_fields=true -cleanup.remove_unused_private_members=false -cleanup.remove_unused_private_methods=true -cleanup.remove_unused_private_types=true -cleanup.sort_members=false -cleanup.sort_members_all=false -cleanup.use_anonymous_class_creation=false -cleanup.use_blocks=true -cleanup.use_blocks_only_for_return_and_throw=false -cleanup.use_lambda=true -cleanup.use_parentheses_in_expressions=false -cleanup.use_this_for_non_static_field_access=false -cleanup.use_this_for_non_static_field_access_only_if_necessary=false -cleanup.use_this_for_non_static_method_access=false -cleanup.use_this_for_non_static_method_access_only_if_necessary=true -cleanup.use_type_arguments=false -cleanup_profile=_Spring REST Docs Cleanup Conventions -cleanup_settings_version=2 -eclipse.preferences.version=1 -editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true -formatter_profile=_Spring REST Docs Java Conventions -formatter_settings_version=12 -org.eclipse.jdt.ui.exception.name=e -org.eclipse.jdt.ui.gettersetter.use.is=false -org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=java;javax;org;com;;\#; -org.eclipse.jdt.ui.javadoc=true -org.eclipse.jdt.ui.keywordthis=false -org.eclipse.jdt.ui.ondemandthreshold=9999 -org.eclipse.jdt.ui.overrideannotation=true -org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= -sp_cleanup.add_default_serial_version_id=true -sp_cleanup.add_generated_serial_version_id=false -sp_cleanup.add_missing_annotations=true -sp_cleanup.add_missing_deprecated_annotations=true -sp_cleanup.add_missing_methods=false -sp_cleanup.add_missing_nls_tags=false -sp_cleanup.add_missing_override_annotations=true -sp_cleanup.add_missing_override_annotations_interface_methods=true -sp_cleanup.add_serial_version_id=false -sp_cleanup.always_use_blocks=true -sp_cleanup.always_use_parentheses_in_expressions=true -sp_cleanup.always_use_this_for_non_static_field_access=true -sp_cleanup.always_use_this_for_non_static_method_access=false -sp_cleanup.convert_to_enhanced_for_loop=false -sp_cleanup.correct_indentation=false -sp_cleanup.format_source_code=true -sp_cleanup.format_source_code_changes_only=false -sp_cleanup.make_local_variable_final=false -sp_cleanup.make_parameters_final=false -sp_cleanup.make_private_fields_final=false -sp_cleanup.make_type_abstract_if_missing_method=false -sp_cleanup.make_variable_declarations_final=false -sp_cleanup.never_use_blocks=false -sp_cleanup.never_use_parentheses_in_expressions=false -sp_cleanup.on_save_use_additional_actions=true -sp_cleanup.organize_imports=true -sp_cleanup.qualify_static_field_accesses_with_declaring_class=false -sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true -sp_cleanup.qualify_static_member_accesses_with_declaring_class=true -sp_cleanup.qualify_static_method_accesses_with_declaring_class=false -sp_cleanup.remove_private_constructors=true -sp_cleanup.remove_trailing_whitespaces=true -sp_cleanup.remove_trailing_whitespaces_all=true -sp_cleanup.remove_trailing_whitespaces_ignore_empty=false -sp_cleanup.remove_unnecessary_casts=true -sp_cleanup.remove_unnecessary_nls_tags=false -sp_cleanup.remove_unused_imports=true -sp_cleanup.remove_unused_local_variables=false -sp_cleanup.remove_unused_private_fields=true -sp_cleanup.remove_unused_private_members=false -sp_cleanup.remove_unused_private_methods=true -sp_cleanup.remove_unused_private_types=true -sp_cleanup.sort_members=false -sp_cleanup.sort_members_all=false -sp_cleanup.use_blocks=true -sp_cleanup.use_blocks_only_for_return_and_throw=false -sp_cleanup.use_parentheses_in_expressions=false -sp_cleanup.use_this_for_non_static_field_access=true -sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=false -sp_cleanup.use_this_for_non_static_method_access=false -sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true From 02fa42445f7c8c84509bac86d3d1ebe569ca9c8f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Jan 2016 11:14:29 +0000 Subject: [PATCH 039/898] Make consistent use of the diamond operator with generic types --- config/eclipse/org.eclipse.jdt.core.prefs | 2 +- .../restdocs/curl/CurlRequestSnippet.java | 6 +++--- .../headers/AbstractHeadersSnippet.java | 8 ++++---- .../restdocs/http/HttpRequestSnippet.java | 4 ++-- .../restdocs/http/HttpResponseSnippet.java | 4 ++-- .../restdocs/hypermedia/LinksSnippet.java | 6 +++--- .../payload/AbstractFieldsSnippet.java | 6 +++--- .../restdocs/payload/JsonContentHandler.java | 4 ++-- .../restdocs/payload/JsonFieldProcessor.java | 6 +++--- .../request/AbstractParametersSnippet.java | 6 +++--- .../restdocs/hypermedia/LinksSnippetTests.java | 4 ++-- .../payload/JsonFieldProcessorTests.java | 18 +++++++++--------- .../restdocs/test/OutputCapture.java | 4 ++-- .../restdocs/test/SnippetMatchers.java | 4 ++-- .../restdocs/mockmvc/IterableEnumeration.java | 4 ++-- ...ckMvcRestDocumentationIntegrationTests.java | 5 ++--- 16 files changed, 45 insertions(+), 46 deletions(-) diff --git a/config/eclipse/org.eclipse.jdt.core.prefs b/config/eclipse/org.eclipse.jdt.core.prefs index 5281c3b46..462feac8f 100644 --- a/config/eclipse/org.eclipse.jdt.core.prefs +++ b/config/eclipse/org.eclipse.jdt.core.prefs @@ -71,7 +71,7 @@ org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 56f1fd4ca..3e903f377 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -49,7 +49,7 @@ public class CurlRequestSnippet extends TemplatedSnippet { private static final Set HEADER_FILTERS; static { - Set headerFilters = new HashSet(); + Set headerFilters = new HashSet<>(); headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); headerFilters.add(new BasicAuthHeaderFilter()); @@ -75,7 +75,7 @@ protected CurlRequestSnippet(Map attributes) { @Override protected Map createModel(Operation operation) { - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("url", getUrl(operation)); model.put("options", getOptions(operation)); return model; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java index f5e2c1269..223913567 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/AbstractHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -76,7 +76,7 @@ protected Map createModel(Operation operation) { private void validateHeaderDocumentation(Operation operation) { List missingHeaders = findMissingHeaders(operation); if (!missingHeaders.isEmpty()) { - List names = new ArrayList(); + List names = new ArrayList<>(); for (HeaderDescriptor headerDescriptor : missingHeaders) { names.add(headerDescriptor.getName()); } @@ -94,7 +94,7 @@ private void validateHeaderDocumentation(Operation operation) { * @return descriptors for the headers that are missing from the operation */ protected List findMissingHeaders(Operation operation) { - List missingHeaders = new ArrayList(); + List missingHeaders = new ArrayList<>(); Set actualHeaders = extractActualHeaders(operation); for (HeaderDescriptor headerDescriptor : this.headerDescriptors) { if (!headerDescriptor.isOptional() @@ -132,7 +132,7 @@ protected final List getHeaderDescriptors() { * @return the model */ protected Map createModelForDescriptor(HeaderDescriptor descriptor) { - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("name", descriptor.getName()); model.put("description", descriptor.getDescription()); model.put("optional", descriptor.isOptional()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 9060ada91..908b2fb84 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -64,7 +64,7 @@ protected HttpRequestSnippet(Map attributes) { @Override protected Map createModel(Operation operation) { - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("method", operation.getRequest().getMethod()); model.put( "path", diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java index 9d2de12c3..c9fd1f718 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpResponseSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -58,7 +58,7 @@ protected HttpResponseSnippet(Map attributes) { protected Map createModel(Operation operation) { OperationResponse response = operation.getResponse(); HttpStatus status = response.getStatus(); - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("responseBody", responseBody(response)); model.put("statusCode", status.value()); model.put("statusReason", status.getReasonPhrase()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 79ae0ee26..b7f868c85 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -101,7 +101,7 @@ protected Map createModel(Operation operation) { private void validate(Map> links) { Set actualRels = links.keySet(); - Set undocumentedRels = new HashSet(actualRels); + Set undocumentedRels = new HashSet<>(actualRels); undocumentedRels.removeAll(this.descriptorsByRel.keySet()); Set requiredRels = new HashSet<>(); @@ -112,7 +112,7 @@ private void validate(Map> links) { } } - Set missingRels = new HashSet(requiredRels); + Set missingRels = new HashSet<>(requiredRels); missingRels.removeAll(actualRels); if (!undocumentedRels.isEmpty() || !missingRels.isEmpty()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 3bf98a83b..3257be17c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -123,7 +123,7 @@ private void validateFieldDocumentation(ContentHandler payloadHandler) { if (message.length() > 0) { message += String.format("%n"); } - List paths = new ArrayList(); + List paths = new ArrayList<>(); for (FieldDescriptor fieldDescriptor : missingFields) { paths.add(fieldDescriptor.getPath()); } @@ -170,7 +170,7 @@ protected final List getFieldDescriptors() { * @return the model */ protected Map createModelForDescriptor(FieldDescriptor descriptor) { - Map model = new HashMap(); + Map model = new HashMap<>(); model.put("path", descriptor.getPath()); model.put("type", descriptor.getType().toString()); model.put("description", descriptor.getDescription()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index fab1d1a5c..b08f14279 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -45,7 +45,7 @@ class JsonContentHandler implements ContentHandler { @Override public List findMissingFields(List fieldDescriptors) { - List missingFields = new ArrayList(); + List missingFields = new ArrayList<>(); Object payload = readContent(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { if (!fieldDescriptor.isOptional() diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index 7ef5df395..5e4ed29d8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -32,7 +32,7 @@ final class JsonFieldProcessor { boolean hasField(JsonFieldPath fieldPath, Object payload) { - final AtomicReference hasField = new AtomicReference(false); + final AtomicReference hasField = new AtomicReference<>(false); traverse(new ProcessingContext(payload, fieldPath), new MatchCallback() { @Override @@ -45,7 +45,7 @@ public void foundMatch(Match match) { } Object extract(JsonFieldPath path, Object payload) { - final List matches = new ArrayList(); + final List matches = new ArrayList<>(); traverse(new ProcessingContext(payload, path), new MatchCallback() { @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 0763505b1..7eb6e11c1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -84,9 +84,9 @@ protected Map createModel(Operation operation) { private void verifyParameterDescriptors(Operation operation) { Set actualParameters = extractActualParameters(operation); Set expectedParameters = this.descriptorsByName.keySet(); - Set undocumentedParameters = new HashSet(actualParameters); + Set undocumentedParameters = new HashSet<>(actualParameters); undocumentedParameters.removeAll(expectedParameters); - Set missingParameters = new HashSet(expectedParameters); + Set missingParameters = new HashSet<>(expectedParameters); missingParameters.removeAll(actualParameters); if (!undocumentedParameters.isEmpty() || !missingParameters.isEmpty()) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index cb9ac2883..07adce26d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -174,7 +174,7 @@ public void linksWithCustomAttributes() throws IOException { private static class StubLinkExtractor implements LinkExtractor { - private MultiValueMap linksByRel = new LinkedMultiValueMap(); + private MultiValueMap linksByRel = new LinkedMultiValueMap<>(); @Override public MultiValueMap extractLinks(OperationResponse response) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index d48212f00..1f9415979 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -138,22 +138,22 @@ public void nonExistentTopLevelField() { @Test(expected = FieldDoesNotExistException.class) public void nonExistentNestedField() { - HashMap payload = new HashMap(); + HashMap payload = new HashMap<>(); payload.put("a", new HashMap()); this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload); } @Test(expected = FieldDoesNotExistException.class) public void nonExistentNestedFieldWhenParentIsNotAMap() { - HashMap payload = new HashMap(); + HashMap payload = new HashMap<>(); payload.put("a", 5); this.fieldProcessor.extract(JsonFieldPath.compile("a.b"), payload); } @Test(expected = FieldDoesNotExistException.class) public void nonExistentFieldWhenParentIsAnArray() { - HashMap payload = new HashMap(); - HashMap alpha = new HashMap(); + HashMap payload = new HashMap<>(); + HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); payload.put("a", alpha); this.fieldProcessor.extract(JsonFieldPath.compile("a.b.c"), payload); @@ -161,21 +161,21 @@ public void nonExistentFieldWhenParentIsAnArray() { @Test(expected = FieldDoesNotExistException.class) public void nonExistentArrayField() { - HashMap payload = new HashMap(); + HashMap payload = new HashMap<>(); this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload); } @Test(expected = FieldDoesNotExistException.class) public void nonExistentArrayFieldAsTypeDoesNotMatch() { - HashMap payload = new HashMap(); + HashMap payload = new HashMap<>(); payload.put("a", 5); this.fieldProcessor.extract(JsonFieldPath.compile("a[]"), payload); } @Test(expected = FieldDoesNotExistException.class) public void nonExistentFieldBeneathAnArray() { - HashMap payload = new HashMap(); - HashMap alpha = new HashMap(); + HashMap payload = new HashMap<>(); + HashMap alpha = new HashMap<>(); alpha.put("b", Arrays.asList(new HashMap())); payload.put("a", alpha); this.fieldProcessor.extract(JsonFieldPath.compile("a.b[].id"), payload); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java index 4aab96685..e7b4ef755 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OutputCapture.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -45,7 +45,7 @@ public class OutputCapture implements TestRule { private ByteArrayOutputStream capturedOutput; - private List> matchers = new ArrayList>(); + private List> matchers = new ArrayList<>(); @Override public Statement apply(final Statement base, Description description) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index d45388a09..97818a52e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -74,7 +74,7 @@ public static AsciidoctorCodeBlockMatcher codeBlock(String language) { private static abstract class AbstractSnippetContentMatcher extends BaseMatcher { - private List lines = new ArrayList(); + private List lines = new ArrayList<>(); protected void addLine(String line) { this.lines.add(line); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java index d80edb13b..d4b9653f2 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/IterableEnumeration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -63,7 +63,7 @@ public void remove() { * @return the iterable */ static Iterable iterable(Enumeration enumeration) { - return new IterableEnumeration(enumeration); + return new IterableEnumeration<>(enumeration); } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 546a17e75..76e3dd677 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -430,8 +430,7 @@ public ResponseEntity> foo() { response.put("links", Arrays.asList(new Link("rel", "href"))); HttpHeaders headers = new HttpHeaders(); headers.add("a", "alpha"); - return new ResponseEntity>(response, headers, - HttpStatus.OK); + return new ResponseEntity<>(response, headers, HttpStatus.OK); } @RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE) From d184425216c809aa28d0be54004bc0b9422ca009 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Jan 2016 12:38:19 +0000 Subject: [PATCH 040/898] Polishing --- .../customizing-requests-and-responses.adoc | 6 +- ...yTest.java => EveryTestPreprocessing.java} | 45 +++++------ ...PerTest.java => PerTestPreprocessing.java} | 20 +++-- .../PrettyPrintingContentModifier.java | 19 +++-- ...cumentationContextPlaceholderResolver.java | 14 +++- .../snippet/TemplatedSnippetTests.java | 77 +++++++++++++++++++ 6 files changed, 134 insertions(+), 47 deletions(-) rename docs/src/test/java/com/example/{PreprocessingEveryTest.java => EveryTestPreprocessing.java} (83%) rename docs/src/test/java/com/example/{PreprocessingPerTest.java => PerTestPreprocessing.java} (83%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 37ca4e0a9..2bc11527c 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -12,7 +12,7 @@ example: [source,java,indent=0] ---- -include::{examples-dir}/com/example/PreprocessingPerTest.java[tags=preprocessing] +include::{examples-dir}/com/example/PerTestPreprocessing.java[tags=preprocessing] ---- <1> Apply a request preprocessor that will remove the header named `Foo`. <2> Apply a response preprocessor that will pretty print its content. @@ -24,7 +24,7 @@ output directories>>: [source,java,indent=0] ---- -include::{examples-dir}/com/example/PreprocessingEveryTest.java[tags=setup] +include::{examples-dir}/com/example/EveryTestPreprocessing.java[tags=setup] ---- <1> Create the `RestDocumentationResultHandler`, configured to preprocess the request and response. @@ -36,7 +36,7 @@ test-specific. For example: [source,java,indent=0] ---- -include::{examples-dir}/com/example/PreprocessingEveryTest.java[tags=use] +include::{examples-dir}/com/example/EveryTestPreprocessing.java[tags=use] ---- <1> Document the links specific to the resource that is being tested <2> The `perform` call will automatically produce the documentation snippets due to the diff --git a/docs/src/test/java/com/example/PreprocessingEveryTest.java b/docs/src/test/java/com/example/EveryTestPreprocessing.java similarity index 83% rename from docs/src/test/java/com/example/PreprocessingEveryTest.java rename to docs/src/test/java/com/example/EveryTestPreprocessing.java index 6199ecb46..b189f8ffd 100644 --- a/docs/src/test/java/com/example/PreprocessingEveryTest.java +++ b/docs/src/test/java/com/example/EveryTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,6 +16,14 @@ package com.example; +import org.junit.Before; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; @@ -23,42 +31,35 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; -import org.junit.Before; -import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +public class EveryTestPreprocessing { -public class PreprocessingEveryTest { - private WebApplicationContext context; - + private MockMvc mockMvc; - + private RestDocumentationResultHandler document; - + // tag::setup[] @Before public void setup() { - this.document = document("{method-name}", // <1> - preprocessRequest(removeHeaders("Foo")), - preprocessResponse(prettyPrint())); - this.mockMvc = MockMvcBuilders - .webAppContextSetup(this.context) - .alwaysDo(this.document) // <2> - .build(); + this.document = document( + "{method-name}", // <1> + preprocessRequest(removeHeaders("Foo")), + preprocessResponse(prettyPrint())); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .alwaysDo(this.document) // <2> + .build(); } - // end::setup[] + + // end::setup[] public void use() throws Exception { // tag::use[] this.document.snippets( // <1> links(linkWithRel("self").description("Canonical self link"))); this.mockMvc.perform(get("/")) // <2> - .andExpect(status().isOk()); + .andExpect(status().isOk()); // end::use[] } diff --git a/docs/src/test/java/com/example/PreprocessingPerTest.java b/docs/src/test/java/com/example/PerTestPreprocessing.java similarity index 83% rename from docs/src/test/java/com/example/PreprocessingPerTest.java rename to docs/src/test/java/com/example/PerTestPreprocessing.java index 6f0ab77ff..504f96c09 100644 --- a/docs/src/test/java/com/example/PreprocessingPerTest.java +++ b/docs/src/test/java/com/example/PerTestPreprocessing.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,27 +16,25 @@ package com.example; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.test.web.servlet.MockMvc; + import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; -import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.test.web.servlet.MockMvc; - -public class PreprocessingPerTest { +public class PerTestPreprocessing { private MockMvc mockMvc; public void general() throws Exception { // tag::preprocessing[] - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()) - .andDo(document("index", - preprocessRequest(removeHeaders("Foo")), // <1> - preprocessResponse(prettyPrint()))); // <2> + this.mockMvc.perform(get("/")).andExpect(status().isOk()) + .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + preprocessResponse(prettyPrint()))); // <2> // end::preprocessing[] } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 16c9471ac..9665fd09c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -98,9 +98,8 @@ private SAXSource createSaxSource(byte[] original) SAXParser parser = parserFactory.newSAXParser(); XMLReader xmlReader = parser.getXMLReader(); xmlReader.setErrorHandler(new SilentErrorHandler()); - SAXSource xmlInput = new SAXSource(xmlReader, new InputSource( - new ByteArrayInputStream(original))); - return xmlInput; + return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream( + original))); } private static final class SilentErrorListener implements ErrorListener { @@ -108,18 +107,18 @@ private static final class SilentErrorListener implements ErrorListener { @Override public void warning(TransformerException exception) throws TransformerException { - + // Suppress } @Override public void error(TransformerException exception) throws TransformerException { - + // Suppress } @Override public void fatalError(TransformerException exception) throws TransformerException { - + // Suppress } } @@ -128,17 +127,17 @@ private static final class SilentErrorHandler implements ErrorHandler { @Override public void warning(SAXParseException exception) throws SAXException { - + // Suppress } @Override public void error(SAXParseException exception) throws SAXException { - + // Suppress } @Override public void fatalError(SAXParseException exception) throws SAXException { - + // Suppress } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index 908f3d5eb..bc12e8725 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -70,6 +70,14 @@ public String resolvePlaceholder(String placeholderName) { if ("step".equals(placeholderName)) { return Integer.toString(this.context.getStepCount()); } + String converted = tryMethodNameConversion(placeholderName); + if (converted != null) { + return converted; + } + return tryClassNameConversion(placeholderName); + } + + private String tryMethodNameConversion(String placeholderName) { if ("methodName".equals(placeholderName)) { return this.context.getTestMethodName(); } @@ -79,6 +87,10 @@ public String resolvePlaceholder(String placeholderName) { if ("method_name".equals(placeholderName)) { return camelCaseToSnakeCase(this.context.getTestMethodName()); } + return null; + } + + private String tryClassNameConversion(String placeholderName) { if ("ClassName".equals(placeholderName)) { return this.context.getTestClass().getSimpleName(); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java new file mode 100644 index 000000000..f356d2da3 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2012-2016 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.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.restdocs.operation.Operation; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link TemplatedSnippet}. + * + * @author Andy Wilkinson + */ +public class TemplatedSnippetTests { + + @Test + public void attributesAreCopied() { + Map attributes = new HashMap<>(); + attributes.put("a", "alpha"); + TemplatedSnippet snippet = new TestTemplatedSnippet(attributes); + attributes.put("b", "bravo"); + assertThat(snippet.getAttributes().size(), is(1)); + assertThat(snippet.getAttributes(), hasEntry("a", (Object) "alpha")); + } + + @Test + public void nullAttributesAreTolerated() { + assertThat(new TestTemplatedSnippet(null).getAttributes(), is(not(nullValue()))); + assertThat(new TestTemplatedSnippet(null).getAttributes().size(), is(0)); + } + + @Test + public void snippetName() { + assertThat( + new TestTemplatedSnippet(Collections.emptyMap()) + .getSnippetName(), + is(equalTo("test"))); + } + + private static class TestTemplatedSnippet extends TemplatedSnippet { + + protected TestTemplatedSnippet(Map attributes) { + super("test", attributes); + } + + @Override + protected Map createModel(Operation operation) { + return null; + } + + } + +} From 97ae7a9ce9d10e3dddb62196f5b3b17edde81307 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Jan 2016 12:51:37 +0000 Subject: [PATCH 041/898] Fix javadoc problem with use of em dash --- .../snippet/RestDocumentationContextPlaceholderResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index bc12e8725..6c3e00be2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -26,7 +26,7 @@ * A {@link PlaceholderResolver} that resolves placeholders using a * {@link RestDocumentationContext}. The following placeholders are supported: *
    - *
  • {@code step} – the {@link RestDocumentationContext#getStepCount() step current + *
  • {@code step} - the {@link RestDocumentationContext#getStepCount() step current * count}. *
  • {@code methodName} - the unmodified name of the * {@link RestDocumentationContext#getTestMethodName() current test method} without From 5f4541bf69ee50ce318878462d93324e77932c6f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 27 Jan 2016 16:55:04 +0000 Subject: [PATCH 042/898] Collapse Maven and Gradle setup into one section with switchable examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds an Asciidoctor extension that post-processes code blocks, collapsing any secondary blocks into the preceding primary block. The primary block is then augmented with a switch. This switch contains one item for each block. Clicking an item causes the associated block’s content to be displayed. Each item is named using its block’s title. The getting started instructions have been updated to take advantage of this new switching support, with the Maven and Gradle instructions being collapsed into a single section. Closes gh-189 --- buildSrc/build.gradle | 7 + .../CodeBlockSwitchExtension.groovy | 28 ++++ .../CodeBlockSwitchPostProcessor.groovy | 39 +++++ ...sciidoctor.extension.spi.ExtensionRegistry | 1 + .../src/main/resources/codeBlockSwitch.css | 23 +++ .../src/main/resources/codeBlockSwitch.js | 45 +++++ docs/src/docs/asciidoc/getting-started.adoc | 154 ++++++++---------- 7 files changed, 211 insertions(+), 86 deletions(-) create mode 100644 buildSrc/build.gradle create mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchExtension.groovy create mode 100644 buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchPostProcessor.groovy create mode 100644 buildSrc/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry create mode 100644 buildSrc/src/main/resources/codeBlockSwitch.css create mode 100644 buildSrc/src/main/resources/codeBlockSwitch.js diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 000000000..ec63ab98b --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,7 @@ +repositories { + mavenCentral() +} + +dependencies { + compile 'org.asciidoctor:asciidoctorj:1.5.2' +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchExtension.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchExtension.groovy new file mode 100644 index 000000000..2dbd03649 --- /dev/null +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchExtension.groovy @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2016 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.asciidoctor + +import org.asciidoctor.Asciidoctor +import org.asciidoctor.extension.spi.ExtensionRegistry + +class CodeBlockSwitchExtension implements ExtensionRegistry { + + @Override + public void register(Asciidoctor asciidoctor) { + asciidoctor.javaExtensionRegistry().postprocessor(new CodeBlockSwitchPostProcessor()); + } +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchPostProcessor.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchPostProcessor.groovy new file mode 100644 index 000000000..e87edf966 --- /dev/null +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/asciidoctor/CodeBlockSwitchPostProcessor.groovy @@ -0,0 +1,39 @@ +/* + * 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.asciidoctor + +import org.asciidoctor.ast.Document +import org.asciidoctor.extension.Postprocessor + +class CodeBlockSwitchPostProcessor extends Postprocessor { + + String process(Document document, String output) { + def css = getClass().getResource("/codeBlockSwitch.css").text + def javascript = getClass().getResource("/codeBlockSwitch.js").text + def replacement = """ + + + +""" + return output.replace("", replacement); + } + +} \ No newline at end of file diff --git a/buildSrc/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry b/buildSrc/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry new file mode 100644 index 000000000..2dcc8a1d3 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry @@ -0,0 +1 @@ +org.springframework.restdocs.asciidoctor.CodeBlockSwitchExtension \ No newline at end of file diff --git a/buildSrc/src/main/resources/codeBlockSwitch.css b/buildSrc/src/main/resources/codeBlockSwitch.css new file mode 100644 index 000000000..6fb1947b1 --- /dev/null +++ b/buildSrc/src/main/resources/codeBlockSwitch.css @@ -0,0 +1,23 @@ +.hidden { + display: none; +} + +.switch { + border-width: 1px 1px 0 1px; + border-style: solid; + border-color: #7a2518; + display: inline-block; +} + +.switch--item { + padding: 10px; + background-color: #ffffff; + color: #7a2518; + display: inline-block; + cursor: pointer; +} + +.switch--item.selected { + background-color: #7a2519; + color: #ffffff; +} \ No newline at end of file diff --git a/buildSrc/src/main/resources/codeBlockSwitch.js b/buildSrc/src/main/resources/codeBlockSwitch.js new file mode 100644 index 000000000..13ac0fc1e --- /dev/null +++ b/buildSrc/src/main/resources/codeBlockSwitch.js @@ -0,0 +1,45 @@ +function addBlockSwitches() { + $('.primary').each(function() { + primary = $(this); + createSwitchItem(primary, createBlockSwitch(primary)).item.addClass("selected"); + primary.children('.title').remove(); + }); + $('.secondary').each(function(idx, node) { + secondary = $(node); + primary = findPrimary(secondary); + switchItem = createSwitchItem(secondary, primary.children('.switch')); + switchItem.content.addClass('hidden'); + findPrimary(secondary).append(switchItem.content); + secondary.remove(); + }); +} + +function createBlockSwitch(primary) { + blockSwitch = $('
    '); + primary.prepend(blockSwitch); + return blockSwitch; +} + +function findPrimary(secondary) { + candidate = secondary.prev(); + while (!candidate.is('.primary')) { + candidate = candidate.prev(); + } + return candidate; +} + +function createSwitchItem(block, blockSwitch) { + blockName = block.children('.title').text(); + content = block.children('.content').first().append(block.next('.colist')); + item = $('
    ' + blockName + '
    '); + item.on('click', '', content, function(e) { + $(this).addClass('selected'); + $(this).siblings().removeClass('selected'); + e.data.siblings('.content').addClass('hidden'); + e.data.removeClass('hidden'); + }); + blockSwitch.append(item); + return {'item': item, 'content': content}; +} + +$(addBlockSwitches); \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 6a652de67..ccc0f7a59 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -28,82 +28,14 @@ The code that produces the generated snippets can be found in `src/test/java`. [[getting-started-build-configuration]] === Build configuration -The first step in using Spring REST Docs is to configure your project's build. - - - -[[getting-started-build-configuration-gradle]] -==== Gradle build configuration - -The {samples}/rest-notes-spring-hateoas[Spring HATEOAS sample] contains a `build.gradle` -file that you may wish to use as a reference. The key parts of the configuration are -described below. - -[source,groovy,indent=0,subs="verbatim,attributes"] ----- - plugins { <1> - id "org.asciidoctor.convert" version "1.5.2" - } - - dependencies { <2> - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' - } - - ext { <3> - snippetsDir = file('build/generated-snippets') - } - - test { <4> - outputs.dir snippetsDir - } - - asciidoctor { <5> - attributes 'snippets': snippetsDir <6> - inputs.dir snippetsDir <7> - dependsOn test <8> - } ----- -<1> Apply the Asciidoctor plugin. -<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. -<3> Configure a property to define the output location for generated snippets. -<4> Configure the `test` task to add the snippets directory as an output. -<5> Configure the `asciidoctor` task -<6> Define an attribute named `snippets` that can be used when including the generated - snippets in your documentation. -<7> Configure the snippets directory as an input. -<8> Make the task depend on the test task so that the tests are run before the - documentation is created. - - -[[getting-started-build-configuration-gradle-packaging-the-documentation]] -==== Packaging the documentation - -You may want to package the generated documentation in your project's jar file, for -example to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as -static content] by Spring Boot. You can do so by configuring the `jar` task to depend on -the `asciidoctor` task and to copy the generated documentation into the jar's static -directory: - -[source,groovy,indent=0] ----- - jar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } - } ----- - - - -[[getting-started-build-configuration-maven]] -==== Maven build configuration - -The {samples}/rest-notes-spring-data-rest[Spring Data REST sample] contains a `pom.xml` -file that you may wish to use as a reference. The key parts of the configuration are -described below. - -[source,xml,indent=0,subs="verbatim,attributes"] +The first step in using Spring REST Docs is to configure your project's build. The +{samples}/rest-notes-spring-hateoas[Spring HATEOAS] and +{samples}/rest-notes-spring-data-rest[Spring Data REST] samples contain a `build.gradle` +and `pom.xml` respectively that you may wish to use as a reference. The key parts of +the configuration are described below. + +[source,xml,indent=0,subs="verbatim,attributes",role="primary"] +.Maven ---- <1> org.springframework.restdocs @@ -150,7 +82,6 @@ described below. - ---- <1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. <2> Configure a property to define the output location for generated snippets. @@ -163,19 +94,55 @@ described below. <> in your project's jar you should use the `prepare-package` phase. -[[getting-started-build-configuration-maven-packaging]] +[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +.Gradle +---- + plugins { <1> + id "org.asciidoctor.convert" version "1.5.2" + } + + dependencies { <2> + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' + } + + ext { <3> + snippetsDir = file('build/generated-snippets') + } + + test { <4> + outputs.dir snippetsDir + } + + asciidoctor { <5> + attributes 'snippets': snippetsDir <6> + inputs.dir snippetsDir <7> + dependsOn test <8> + } +---- +<1> Apply the Asciidoctor plugin. +<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. +<3> Configure a property to define the output location for generated snippets. +<4> Configure the `test` task to add the snippets directory as an output. +<5> Configure the `asciidoctor` task +<6> Define an attribute named `snippets` that can be used when including the generated + snippets in your documentation. +<7> Configure the snippets directory as an input. +<8> Make the task depend on the test task so that the tests are run before the + documentation is created. + + +[[getting-started-build-configuration-gradle-packaging-the-documentation]] ==== Packaging the documentation You may want to package the generated documentation in your project's jar file, for example to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as -static content] by Spring Boot. +static content] by Spring Boot. To do so, configure your project's build so that: -First, configure the Asciidoctor plugin so that it runs in the `prepare-package` phase, as -<>. Now configure -Maven's resources plugin to copy the generated documentation into a location where it'll -be included in the project's jar: +1. The documentation is generated before the jar is built +2. The generated documentation is included in the jar -[source,xml,indent=0] +[source,xml,indent=0,role="primary",role="primary"] +.Maven ---- <1> org.asciidoctor @@ -192,7 +159,7 @@ be included in the project's jar: copy-resources - + <3> ${project.build.outputDirectory}/static/docs @@ -211,7 +178,22 @@ be included in the project's jar: <1> The existing declaration for the Asciidoctor plugin. <2> The resource plugin must be declared after the Asciidoctor plugin as they are bound to the same phase (`prepare-package`) and the resource plugin must run after the -Asciidoctor plugin. +Asciidoctor plugin to ensure that the documentation is generated before it's copied. +<3> Copy the generated documentation into the build output's `static/docs` directory, +from where it will be included in the jar file. + +[source,groovy,indent=0,role="secondary"] +.Gradle +---- + jar { + dependsOn asciidoctor <1> + from ("${asciidoctor.outputDir}/html5") { <2> + into 'static/docs' + } + } +---- +<1> Ensure that the documentation has been generated before the jar is built. +<2> Copy the generated documentation into the jar's `static/docs` directory. From d55b5b7738f1487a991ee341b71431184707376e Mon Sep 17 00:00:00 2001 From: Grant Hutchins Date: Wed, 27 Jan 2016 09:37:37 -0600 Subject: [PATCH 043/898] Fix typo Closes gh-188 --- docs/src/docs/asciidoc/getting-started.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 6a652de67..0232564be 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -283,7 +283,7 @@ a `RestDocumentationResultHandler`. An instance of this class can be obtained fr static `document` method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. -By default, three snippets a written: +By default, three snippets are written: * `/index/curl-request.adoc` * `/index/http-request.adoc` From 9dfa0b538509b1336a0afa3f26e3fe750d93a7cb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Jan 2016 11:15:45 +0000 Subject: [PATCH 044/898] Polish PatternReplacingContentModifier --- .../PatternReplacingContentModifier.java | 16 ++++++++-------- .../PatternReplacingContentModifierTests.java | 13 +++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java index 58d4e3a7a..7fe67754c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -35,8 +35,8 @@ class PatternReplacingContentModifier implements ContentModifier { private final String replacement; /** - * Creates a new {@link PatternReplacingContentModifier} that will replace occurences - * the given {@code pattern} with the given {@code replacement}. + * Creates a new {@link PatternReplacingContentModifier} that will replace occurrences + * of the given {@code pattern} with the given {@code replacement}. * * @param pattern the pattern * @param replacement the replacement @@ -56,7 +56,7 @@ public byte[] modifyContent(byte[] content, MediaType contentType) { original = new String(content); } Matcher matcher = this.pattern.matcher(original); - StringBuilder buffer = new StringBuilder(); + StringBuilder builder = new StringBuilder(); int previous = 0; while (matcher.find()) { String prefix; @@ -68,13 +68,13 @@ public byte[] modifyContent(byte[] content, MediaType contentType) { prefix = original.substring(previous, matcher.start()); previous = matcher.end(); } - buffer.append(prefix); - buffer.append(this.replacement); + builder.append(prefix); + builder.append(this.replacement); } if (previous < original.length()) { - buffer.append(original.substring(previous)); + builder.append(original.substring(previous)); } - return buffer.toString().getBytes(); + return builder.toString().getBytes(); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index 1046cad9a..9d37ed78e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -46,6 +46,19 @@ public void patternsAreReplaced() throws Exception { is(equalTo("{\"id\" : \"<>\"}".getBytes()))); } + @Test + public void contentThatDoesNotMatchIsUnchanged() throws Exception { + Pattern pattern = Pattern.compile( + "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", + Pattern.CASE_INSENSITIVE); + PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( + pattern, "<>"); + assertThat( + contentModifier.modifyContent( + "{\"id\" : \"CA76-ED42-11CE-BACD\"}".getBytes(), null), + is(equalTo("{\"id\" : \"CA76-ED42-11CE-BACD\"}".getBytes()))); + } + @Test public void encodingIsPreserved() { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; From 130b411e2ad5e5305b8248ad672a97ab9439c353 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 7 Sep 2015 14:36:42 +0100 Subject: [PATCH 045/898] Add support for using REST Assured to generate documentation snippets This commit adds a new module, spring-restdocs-restassured, that can be used to generate documentation snippets when testing a service with REST Assured. Please refer to the updated reference documentation for details. Thanks to Johan Haleby for making a change to REST Assured so that path parameters could be documented. Closes gh-102 --- build.gradle | 1 + config/checkstyle/checkstyle.xml | 2 +- docs/build.gradle | 1 + docs/src/docs/asciidoc/configuration.adoc | 41 +- .../customizing-requests-and-responses.adoc | 66 +++- .../docs/asciidoc/documenting-your-api.adoc | 168 ++++++-- docs/src/docs/asciidoc/getting-started.adoc | 77 ++-- docs/src/docs/asciidoc/introduction.adoc | 7 +- .../test/java/com/example/Constraints.java | 1 + .../CustomDefaultSnippets.java} | 10 +- .../example/{ => mockmvc}/CustomEncoding.java | 10 +- .../{ => mockmvc}/CustomUriConfiguration.java | 6 +- .../{ => mockmvc}/EveryTestPreprocessing.java | 15 +- .../ExampleApplicationTests.java | 13 +- .../example/{ => mockmvc}/HttpHeaders.java | 30 +- .../com/example/{ => mockmvc}/Hypermedia.java | 4 +- .../example/{ => mockmvc}/InvokeService.java | 4 +- .../ParameterizedOutput.java} | 24 +- .../example/{ => mockmvc}/PathParameters.java | 4 +- .../com/example/{ => mockmvc}/Payload.java | 22 +- .../{ => mockmvc}/PerTestPreprocessing.java | 6 +- .../{ => mockmvc}/RequestParameters.java | 6 +- .../restassured/CustomDefaultSnippets.java | 46 +++ .../example/restassured/CustomEncoding.java | 45 +++ .../restassured/EveryTestPreprocessing.java | 72 ++++ .../restassured/ExampleApplicationTests.java | 44 +++ .../com/example/restassured/HttpHeaders.java | 51 +++ .../com/example/restassured/Hypermedia.java | 54 +++ .../example/restassured/InvokeService.java | 38 ++ .../restassured/ParameterizedOutput.java | 46 +++ .../example/restassured/PathParameters.java | 41 ++ .../java/com/example/restassured/Payload.java | 75 ++++ .../restassured/PerTestPreprocessing.java | 44 +++ .../restassured/RequestParameters.java | 53 +++ settings.gradle | 3 +- .../restdocs/config}/AbstractConfigurer.java | 18 +- .../config/AbstractNestedConfigurer.java | 43 ++ .../restdocs/config}/NestedConfigurer.java | 12 +- .../config/RestDocumentationConfigurer.java | 132 +++++++ .../restdocs/config}/SnippetConfigurer.java | 49 ++- .../request/PathParametersSnippet.java | 7 +- .../mockmvc/MockMvcRestDocumentation.java | 6 +- .../MockMvcRestDocumentationConfigurer.java | 115 ++++++ ...rer.java => MockMvcSnippetConfigurer.java} | 27 +- .../RestDocumentationMockMvcConfigurer.java | 198 ---------- .../RestDocumentationResultHandler.java | 27 +- .../restdocs/mockmvc/UriConfigurer.java | 30 +- ...kMvcRestDocumentationConfigurerTests.java} | 16 +- ...kMvcRestDocumentationIntegrationTests.java | 8 +- spring-restdocs-restassured/build.gradle | 16 + .../RestAssuredOperationRequestFactory.java | 108 +++++ .../RestAssuredOperationResponseFactory.java | 53 +++ .../RestAssuredRestDocumentation.java | 108 +++++ ...estAssuredRestDocumentationConfigurer.java | 76 ++++ .../RestAssuredSnippetConfigurer.java | 48 +++ .../restassured/RestDocumentationFilter.java | 164 ++++++++ .../preprocess/RestAssuredPreprocessors.java | 48 +++ .../UriModifyingOperationPreprocessor.java | 249 ++++++++++++ .../operation/preprocess/package-info.java | 22 ++ .../restdocs/restassured/package-info.java | 21 + ...stAssuredOperationRequestFactoryTests.java | 251 ++++++++++++ ...suredRestDocumentationConfigurerTests.java | 91 +++++ ...uredRestDocumentationIntegrationTests.java | 374 ++++++++++++++++++ ...riModifyingOperationPreprocessorTests.java | 343 ++++++++++++++++ .../src/test/resources/body.txt | 1 + .../restdocs/templates/curl-request.snippet | 1 + 66 files changed, 3360 insertions(+), 432 deletions(-) rename docs/src/test/java/com/example/{CustomDefaultSnippetsConfiguration.java => mockmvc/CustomDefaultSnippets.java} (86%) rename docs/src/test/java/com/example/{ => mockmvc}/CustomEncoding.java (87%) rename docs/src/test/java/com/example/{ => mockmvc}/CustomUriConfiguration.java (94%) rename docs/src/test/java/com/example/{ => mockmvc}/EveryTestPreprocessing.java (84%) rename docs/src/test/java/com/example/{ => mockmvc}/ExampleApplicationTests.java (85%) rename docs/src/test/java/com/example/{ => mockmvc}/HttpHeaders.java (63%) rename docs/src/test/java/com/example/{ => mockmvc}/Hypermedia.java (96%) rename docs/src/test/java/com/example/{ => mockmvc}/InvokeService.java (93%) rename docs/src/test/java/com/example/{AlwaysDo.java => mockmvc/ParameterizedOutput.java} (84%) rename docs/src/test/java/com/example/{ => mockmvc}/PathParameters.java (94%) rename docs/src/test/java/com/example/{ => mockmvc}/Payload.java (83%) rename docs/src/test/java/com/example/{ => mockmvc}/PerTestPreprocessing.java (90%) rename docs/src/test/java/com/example/{ => mockmvc}/RequestParameters.java (96%) create mode 100644 docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java create mode 100644 docs/src/test/java/com/example/restassured/CustomEncoding.java create mode 100644 docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java create mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationTests.java create mode 100644 docs/src/test/java/com/example/restassured/HttpHeaders.java create mode 100644 docs/src/test/java/com/example/restassured/Hypermedia.java create mode 100644 docs/src/test/java/com/example/restassured/InvokeService.java create mode 100644 docs/src/test/java/com/example/restassured/ParameterizedOutput.java create mode 100644 docs/src/test/java/com/example/restassured/PathParameters.java create mode 100644 docs/src/test/java/com/example/restassured/Payload.java create mode 100644 docs/src/test/java/com/example/restassured/PerTestPreprocessing.java create mode 100644 docs/src/test/java/com/example/restassured/RequestParameters.java rename {spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc => spring-restdocs-core/src/main/java/org/springframework/restdocs/config}/AbstractConfigurer.java (59%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java rename {spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc => spring-restdocs-core/src/main/java/org/springframework/restdocs/config}/NestedConfigurer.java (72%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java rename {spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc => spring-restdocs-core/src/main/java/org/springframework/restdocs/config}/SnippetConfigurer.java (61%) create mode 100644 spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java rename spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/{AbstractNestedConfigurer.java => MockMvcSnippetConfigurer.java} (65%) delete mode 100644 spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java rename spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/{RestDocumentationConfigurerTests.java => MockMvcRestDocumentationConfigurerTests.java} (85%) create mode 100644 spring-restdocs-restassured/build.gradle create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java create mode 100644 spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java create mode 100644 spring-restdocs-restassured/src/test/resources/body.txt create mode 100644 spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet diff --git a/build.gradle b/build.gradle index 30c5a2b05..a286bdeed 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,7 @@ subprojects { } dependencies { dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6.1' + dependency 'com.jayway.restassured:rest-assured:2.8.0' dependency 'com.samskivert:jmustache:1.11' dependency 'commons-codec:commons-codec:1.10' dependency 'javax.servlet:javax.servlet-api:3.1.0' diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index b7908d746..ebc91d6ff 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/build.gradle b/docs/build.gradle index 5241018c0..2816cd284 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -4,6 +4,7 @@ plugins { dependencies { testCompile project(':spring-restdocs-mockmvc') + testCompile project(':spring-restdocs-restassured') testCompile 'javax.validation:validation-api' } diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index ae1b601f8..239588003 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -6,7 +6,12 @@ [[configuration-uris]] === Documented URIs -The default configuration for URIs documented by Spring REST Docs is: +NOTE: As REST Assured tests a service by making actual HTTP requests, the documented +URIs cannot be customized in this way. You should use the +<> instead. + +When using MockMvc, the default configuration for URIs documented by Spring REST Docs is: |=== |Setting |Default @@ -21,12 +26,12 @@ The default configuration for URIs documented by Spring REST Docs is: |`8080` |=== -This configuration is applied by `RestDocumentationMockMvcConfigurer`. You can use its API +This configuration is applied by `MockMvcRestDocumentationConfigurer`. You can use its API to change one or more of the defaults to suit your needs: [source,java,indent=0] ---- -include::{examples-dir}/com/example/CustomUriConfiguration.java[tags=custom-uri-configuration] +include::{examples-dir}/com/example/mockmvc/CustomUriConfiguration.java[tags=custom-uri-configuration] ---- TIP: To configure a request's context path, use the `contextPath` method on @@ -38,12 +43,19 @@ TIP: To configure a request's context path, use the `contextPath` method on === Snippet encoding The default encoding used by Asciidoctor is `UTF-8`. Spring REST Docs adopts the same -default for the snippets that it generates. If you require an encoding other than `UTF-8`, -use `RestDocumentationMockMvcConfigurer` to configure it: +default for the snippets that it generates. You can change the default snippet encoding +using the `RestDocumentationConfigurer` API. For example, to use `ISO-8859-1`: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/CustomEncoding.java[tags=custom-encoding] +---- + +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/CustomEncoding.java[tags=custom-encoding] +include::{examples-dir}/com/example/restassured/CustomEncoding.java[tags=custom-encoding] ---- @@ -57,11 +69,18 @@ Three snippets are produced by default: - `http-request` - `http-response` -This default configuration is applied by `RestDocumentationMockMvcConfigurer`. You can use -its API to change the configuration. For example, to only produce the `curl-request` +You can change the default snippet configuration during setup using the +`RestDocumentationConfigurer` API. For example, to only produce the `curl-request` snippet by default: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/CustomDefaultSnippets.java[tags=custom-default-snippets] +---- + +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/CustomDefaultSnippetsConfiguration.java[tags=custom-default-snippets] +include::{examples-dir}/com/example/restassured/CustomDefaultSnippets.java[tags=custom-default-snippets] ---- \ No newline at end of file diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 2bc11527c..900106388 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -10,9 +10,18 @@ and/or an `OperationResponsePreprocessor`. Instances can be obtained using the static `preprocessRequest` and `preprocessResponse` methods on `Preprocessors`. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/PerTestPreprocessing.java[tags=preprocessing] +include::{examples-dir}/com/example/mockmvc/PerTestPreprocessing.java[tags=preprocessing] +---- +<1> Apply a request preprocessor that will remove the header named `Foo`. +<2> Apply a response preprocessor that will pretty print its content. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/PerTestPreprocessing.java[tags=preprocessing] ---- <1> Apply a request preprocessor that will remove the header named `Foo`. <2> Apply a response preprocessor that will pretty print its content. @@ -22,25 +31,44 @@ so by configuring the preprocessors in your `@Before` method and using the <>: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/EveryTestPreprocessing.java[tags=setup] +include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=setup] ---- -<1> Create the `RestDocumentationResultHandler`, configured to preprocess the request +<1> Create a `RestDocumentationResultHandler`, configured to preprocess the request and response. -<2> Create the `MockMvc` instance, configured to always call the documentation result +<2> Create a `MockMvc` instance, configured to always call the documentation result handler. -Then, in each test, the `RestDocumentationResultHandler` can be configured with anything -test-specific. For example: +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=setup] +---- +<1> Create a `RestDocumentationFilter`, configured to preprocess the request + and response. +<2> Create a `RequestSpecification` instance, configured to always call the documentation + filter. + +Then, in each test, any configuration specific to that test can be performed. For example: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=use] +---- +<1> Document the links specific to the resource that is being tested +<2> The request and response will be preprocessed due to the use of `alwaysDo` above. -[source,java,indent=0] +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/EveryTestPreprocessing.java[tags=use] +include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=use] ---- <1> Document the links specific to the resource that is being tested -<2> The `perform` call will automatically produce the documentation snippets due to the - use of `alwaysDo` above. +<2> The request and response will be preprocessed due to the configuration of the +`RequestSpecification` in the `setUp` method. Various built in preprocessors, including those illustrated above, are available via the static methods on `Preprocessors`. See <> for further details. @@ -86,6 +114,20 @@ from the request or response. replacing content in a request or response. Any occurrences of a regular expression are replaced. + + +[[customizing-requests-and-responses-preprocessors-modify-uris]] +==== Modifying URIs + +TIP: If you are using MockMvc, URIs should be customized by <>. + +`modifyUris` on `RestAssuredPreprocessors` can be used to modify any URIs in a request +or a response. When using REST Assured, this allows you to customize the URIs that appear +in the documentation while testing a local instance of the service. + + + [[customizing-requests-and-responses-preprocessors-writing-your-own]] ==== Writing your own preprocessor diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 25fc52b15..c68b9d43a 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -11,9 +11,22 @@ This section provides more details about using Spring REST Docs to document your Spring REST Docs provides support for documenting the links in a https://en.wikipedia.org/wiki/HATEOAS[Hypermedia-based] API: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Hypermedia.java[tag=links] +---- +<1> Configure Spring REST docs to 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`. + +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/Hypermedia.java[tag=links] +include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=links] ---- <1> Configure Spring REST docs to produce a snippet describing the response's links. Uses the static `links` method on @@ -47,14 +60,23 @@ Two link formats are understood by default: 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 `links`. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/Hypermedia.java[tag=explicit-extractor] +include::{examples-dir}/com/example/mockmvc/Hypermedia.java[tag=explicit-extractor] ---- <1> Indicate that the links are in HAL format. Uses the static `halLinks` method on `org.springframework.restdocs.hypermedia.HypermediaDocumentation`. -If your API represents its links in a format other than Atom or HAL you can provide your +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=explicit-extractor] +---- +<1> Indicate that the links are in HAL format. Uses the static `halLinks` method on +`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 response. @@ -65,9 +87,22 @@ In addition to the hypermedia-specific support <>, support for general documentation of request and response payloads is also provided. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/Payload.java[tags=response] +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=response] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the response + payload. To document a request `requestFields` can be used. Both are static methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<3> Expect a field with the path `contact.email`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=response] ---- <1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. To document a request `requestFields` can be used. Both are static methods on @@ -223,9 +258,17 @@ The type can also be set explicitly using the `type(Object)` method on in the documentation. Typically, one of the values enumerated by `JsonFieldType` will be used: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/Payload.java[tags=explicit-type] +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=explicit-type] +---- +<1> Set the field's type to `string`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=explicit-type] ---- <1> Set the field's type to `string`. @@ -255,9 +298,10 @@ method will be used in the documentation. A request's parameters can be documented using `requestParameters`. Request parameters can be included in a `GET` request's query string. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/RequestParameters.java[tags=request-parameters-query-string] +include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-parameters-query-string] ---- <1> Perform a `GET` request with two parameters, `page` and `per_page` in the query string. @@ -268,14 +312,37 @@ include::{examples-dir}/com/example/RequestParameters.java[tags=request-paramete `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the `per_page` parameter. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-query-string] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's parameters. + Uses the static `requestParameters` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the `page` parameter. Uses the static `parameterWithName` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the `per_page` parameter. +<4> Perform a `GET` request with two parameters, `page` and `per_page` in the query + string. + Request parameters can also be included as form data in the body of a POST request: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/RequestParameters.java[tags=request-parameters-form-data] +include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request-parameters-form-data] ---- <1> Perform a `POST` request with a single parameter, `username`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestParameters.java[tags=request-parameters-form-data] +---- +<1> Configure the `username` parameter. +<2> Perform the `POST` request. + In both cases, the result is a snippet named `request-parameters.adoc` that contains a table describing the parameters that are supported by the resource. @@ -294,9 +361,10 @@ above. A request's path parameters can be documented using `pathParameters`. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] +include::{examples-dir}/com/example/mockmvc/PathParameters.java[tags=path-parameters] ---- <1> Perform a `GET` request with two path parameters, `latitude` and `longitude`. <2> Configure Spring REST Docs to produce a snippet describing the request's path @@ -306,6 +374,19 @@ include::{examples-dir}/com/example/PathParameters.java[tags=path-parameters] `org.springframework.restdocs.request.RequestDocumentation`. <4> Document the parameter named `longitude`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/PathParameters.java[tags=path-parameters] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's path + parameters. Uses the static `pathParameters` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the parameter named `latitude`. Uses the static `parameterWithName` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the parameter named `longitude`. +<4> Perform a `GET` request with two path parameters, `latitude` and `longitude`. + The result is a snippet named `path-parameters.adoc` that contains a table describing the path parameters that are supported by the resource. @@ -329,9 +410,10 @@ above. The headers in a request or response can be documented using `requestHeaders` and `responseHeaders` respectively. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/HttpHeaders.java[tags=headers] +include::{examples-dir}/com/example/mockmvc/HttpHeaders.java[tags=headers] ---- <1> Perform a `GET` request with an `Authorization` header that uses basic authentication <2> Configure Spring REST Docs to produce a snippet describing the request's headers. @@ -342,6 +424,20 @@ include::{examples-dir}/com/example/HttpHeaders.java[tags=headers] <4> Produce a snippet describing the response's headers. Uses the static `responseHeaders` method on `org.springframework.restdocs.headers.HeaderDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/HttpHeaders.java[tags=headers] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's headers. + Uses the static `requestHeaders` method on + `org.springframework.restdocs.headers.HeaderDocumentation`. +<2> Document the `Authorization` header. Uses the static `headerWithName` method on + `org.springframework.restdocs.headers.HeaderDocumentation. +<3> Produce a snippet describing the response's headers. Uses the static `responseHeaders` + method on `org.springframework.restdocs.headers.HeaderDocumentation`. +<4> Configure the request with an `Authorization` header that uses basic authentication + The result is a snippet named `request-headers.adoc` and a snippet named `response-headers.adoc`. Each contains a table describing the headers. @@ -436,8 +532,7 @@ class in the Spring HATEOAS-based sample illustrates the latter approach. [[documenting-your-api-default-snippets]] === Default snippets -A number of snippets are produced automatically when you document a call to -`MockMvc.perform`: +A number of snippets are produced automatically when you document a request and response. [cols="1,3"] |=== @@ -489,23 +584,30 @@ are supported: | The nome of the test class, formatted using snake_case | {step} -| The count of calls to MockMvc.perform in the current test +| The count of calls made to the service in the current test |=== For example, `document("{class-name}/{method-name}")` in a test method named `creatingANote` on the test class `GettingStartedDocumentation`, will write snippets into a directory named `getting-started-documentation/creating-a-note`. -A parameterized output directory is particularly useful in combination with Spring MVC -Test's `alwaysDo` functionality. It allows documentation to be configured once in a setup -method: +A parameterized output directory is particularly useful in combination with an `@Before` +method. It allows documentation to be configured once in a setup method and then reused +in every test in the class: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/AlwaysDo.java[tags=always-do] +include::{examples-dir}/com/example/mockmvc/ParameterizedOutput.java[tags=parameterized-output] ---- -With this configuration in place, every call to `MockMvc.perform` will produce +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/ParameterizedOutput.java[tags=parameterized-output] +---- + +With this configuration in place, every call to the service you are testing will produce the <> without any further configuration. Take a look at the `GettingStartedDocumentation` classes in each of the sample applications to see this functionality in action. @@ -549,9 +651,19 @@ A concrete example of the above is the addition of a constraints column and a ti documenting request fields. The first step is to provide a `constraints` attribute for each field that you are documenting and to provide a `title` attribute: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=constraints] +---- +<1> Configure the `title` attribute for the request fields snippet +<2> Set the `constraints` attribute for the `name` field +<3> Set the `constraints` attribute for the `email` field + +[source,java,indent=0,role="secondary"] +.REST Assured ---- -include::{examples-dir}/com/example/Payload.java[tags=constraints] +include::{examples-dir}/com/example/restassured/Payload.java[tags=constraints] ---- <1> Configure the `title` attribute for the request fields snippet <2> Set the `constraints` attribute for the `name` field diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 8f8a11ca3..2dc9bc92c 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -83,7 +83,9 @@ the configuration are described below. ---- -<1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. +<1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. If you want to use + REST Assured rather than MockMvc, add a dependency on `spring-restdocs-restassured` + instead. <2> Configure a property to define the output location for generated snippets. <3> Add the SureFire plugin and configure it to include files whose names end with `Documentation.java`. @@ -120,7 +122,9 @@ project's jar you should use the `prepare-package` phase. } ---- <1> Apply the Asciidoctor plugin. -<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. +<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. If + you want to use REST Assured rather than MockMvc, add a dependency on + `spring-restdocs-restassured` instead. <3> Configure a property to define the output location for generated snippets. <4> Configure the `test` task to add the snippets directory as an output. <5> Configure the `asciidoctor` task @@ -199,14 +203,16 @@ from where it will be included in the jar file. [[getting-started-documentation-snippets]] === Generating documentation snippets -Spring REST Docs uses {spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test -framework] to make requests to the service that you are documenting. It then produces -documentation snippets for request and resulting response. +Spring REST Docs uses JUnit and +{spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test framework] or +http://www.rest-assured.io[REST Assured] to make requests to the service that you are +documenting. It then produces documentation snippets for the request and the resulting +response. [[getting-started-documentation-snippets-setup]] -==== Setting up Spring MVC test +==== Setting up your tests The first step in generating documentation snippets is to declare a `public` `RestDocumentation` field that's annotated as a JUnit `@Rule`. The `RestDocumentation` @@ -214,47 +220,59 @@ rule is configured with the output directory into which generated snippets shoul written. This output directory should match the snippets directory that you have configured in your `build.gradle` or `pom.xml` file. -For Maven (`pom.xml` that will typically be `target/generated-snippets`: +For Maven (`pom.xml` that will typically be `target/generated-snippets` and for +Gradle (`build.gradle`) it will typically be `build/generated-snippets`: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.Maven ---- @Rule public RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); ---- -And for Gradle (`build.gradle`) it will typically be `build/generated-snippets`: - -[source,java,indent=0] +[source,java,indent=0,role="secondary"] +.Gradle ---- @Rule public RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); ---- -Next, provide an `@Before` method that creates a `MockMvc` instance: +Next, provide an `@Before` method to configure MockMvc or REST Assured: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/ExampleApplicationTests.java[tags=mock-mvc-setup] +include::{examples-dir}/com/example/mockmvc/ExampleApplicationTests.java[tags=setup] ---- - -The `MockMvc` instance is configured using a `RestDocumentationMockMvcConfigurer`. An +<1> The `MockMvc` instance is configured using a `MockMvcRestDocumentationConfigurer`. An instance of this class can be obtained from the static `documentationConfiguration()` method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. -`RestDocumentationMockMvcConfigurer` applies sensible defaults and also provides an API -for customizing the configuration. Refer to the -<> for more information. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/ExampleApplicationTests.java[tags=setup] +---- +<1> REST Assured is configured by adding a `RestAssuredRestDocumentationConfigurer` as a +`Filter`. An instance of this class can be obtained from the static +`documentationConfiguration()` method on +`org.springframework.restdocs.restassured.RestAssuredRestDocumentation`. + +The configurer applies sensible defaults and also provides an API for customizing the +configuration. Refer to the <> for more information. [[getting-started-documentation-snippets-invoking-the-service]] ==== Invoking the RESTful service -Now that a `MockMvc` instance has been created, it can be used to invoke the RESTful +Now that the testing framework has been configured, it can be used to invoke the RESTful service and document the request and response. For example: -[source,java,indent=0] +[source,java,indent=0,role="primary"] +.MockMvc ---- -include::{examples-dir}/com/example/InvokeService.java[tags=invoke-service] +include::{examples-dir}/com/example/mockmvc/InvokeService.java[tags=invoke-service] ---- <1> Invoke the root (`/`) of the service and indicate that an `application/json` response is required. @@ -265,6 +283,21 @@ a `RestDocumentationResultHandler`. An instance of this class can be obtained fr static `document` method on `org.springframework.restdocs.mockmvc.MockMvcRestDocumentation`. +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/InvokeService.java[tags=invoke-service] +---- +<1> Apply the specification that was initialised in the `@Before` method. +<2> Indicate that an `application/json` response is required. +<3> Document the call to the service, writing the snippets into a directory named `index` +that will be located beneath the configured output directory. The snippets are written by +a `RestDocumentationFilter`. An instance of this class can be obtained from the +static `document` method on +`org.springframework.restdocs.restassured.RestAssuredRestDocumentation`. +<4> Invoke the root (`/`) of the service. +<5> Assert that the service produce the expected response. + By default, three snippets are written: * `/index/curl-request.adoc` diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index 9590ac358..3bfaef7d3 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -10,9 +10,10 @@ http://asciidoctor.org[Asciidoctor]. Asciidoctor processes plain text and produc HTML, styled and layed out to suit your needs. Spring REST Docs makes use of snippets produced by tests written with -{spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test]. This test-driven -approach helps to guarantee the accuracy of your service's documentation. If a snippet is -incorrect the test that produces it will fail. +{spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test] or +http://www.rest-assured.io[REST Assured]. This test-driven approach helps to guarantee the +accuracy of your service's documentation. If a snippet is incorrect the test that produces +it will fail. Documenting a RESTful service is largely about describing its resources. Two key parts of each resource's description are the details of the HTTP requests that it consumes diff --git a/docs/src/test/java/com/example/Constraints.java b/docs/src/test/java/com/example/Constraints.java index 5bcea8595..254a9577b 100644 --- a/docs/src/test/java/com/example/Constraints.java +++ b/docs/src/test/java/com/example/Constraints.java @@ -41,6 +41,7 @@ static class UserInput { @NotNull @Size(min = 8) String password; + } // end::constraints[] diff --git a/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java similarity index 86% rename from docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java rename to docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index a964c9646..fcd6a742a 100644 --- a/docs/src/test/java/com/example/CustomDefaultSnippetsConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; @@ -27,7 +27,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -public class CustomDefaultSnippetsConfiguration { +public class CustomDefaultSnippets { @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build"); @@ -41,8 +41,8 @@ public class CustomDefaultSnippetsConfiguration { public void setUp() { // tag::custom-default-snippets[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).snippets() - .withDefaults(curlRequest())) + .apply(documentationConfiguration(this.restDocumentation) + .snippets().withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] } diff --git a/docs/src/test/java/com/example/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java similarity index 87% rename from docs/src/test/java/com/example/CustomEncoding.java rename to docs/src/test/java/com/example/mockmvc/CustomEncoding.java index 4490e8a4d..80130ef18 100644 --- a/docs/src/test/java/com/example/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; @@ -27,7 +27,7 @@ import org.springframework.web.context.WebApplicationContext; public class CustomEncoding { - + @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build"); @@ -40,8 +40,8 @@ public class CustomEncoding { public void setUp() { // tag::custom-encoding[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation).snippets() - .withEncoding("ISO-8859-1")) + .apply(documentationConfiguration(this.restDocumentation) + .snippets().withEncoding("ISO-8859-1")) .build(); // end::custom-encoding[] } diff --git a/docs/src/test/java/com/example/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java similarity index 94% rename from docs/src/test/java/com/example/CustomUriConfiguration.java rename to docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index b70d4e08e..9fe6130c9 100644 --- a/docs/src/test/java/com/example/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; @@ -27,7 +27,7 @@ import org.springframework.web.context.WebApplicationContext; public class CustomUriConfiguration { - + @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build"); diff --git a/docs/src/test/java/com/example/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java similarity index 84% rename from docs/src/test/java/com/example/EveryTestPreprocessing.java rename to docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index b189f8ffd..e1f9b9961 100644 --- a/docs/src/test/java/com/example/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -25,6 +27,7 @@ import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; @@ -34,20 +37,24 @@ public class EveryTestPreprocessing { + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation( + "target/generated-snippets"); + private WebApplicationContext context; + // tag::setup[] private MockMvc mockMvc; private RestDocumentationResultHandler document; - // tag::setup[] @Before public void setup() { - this.document = document( - "{method-name}", // <1> + this.document = document("{method-name}", // <1> preprocessRequest(removeHeaders("Foo")), preprocessResponse(prettyPrint())); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(this.document) // <2> .build(); } diff --git a/docs/src/test/java/com/example/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java similarity index 85% rename from docs/src/test/java/com/example/ExampleApplicationTests.java rename to docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index 70fb8e188..97abfa059 100644 --- a/docs/src/test/java/com/example/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import org.junit.Before; import org.junit.Rule; @@ -29,8 +29,9 @@ public class ExampleApplicationTests { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); - // tag::mock-mvc-setup[] + public final RestDocumentation restDocumentation = new RestDocumentation( + "target/generated-snippets"); + // tag::setup[] @Autowired private WebApplicationContext context; @@ -39,8 +40,8 @@ public class ExampleApplicationTests { @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) + .apply(documentationConfiguration(this.restDocumentation)) // <1> .build(); } - // end::mock-mvc-setup[] + // end::setup[] } diff --git a/docs/src/test/java/com/example/HttpHeaders.java b/docs/src/test/java/com/example/mockmvc/HttpHeaders.java similarity index 63% rename from docs/src/test/java/com/example/HttpHeaders.java rename to docs/src/test/java/com/example/mockmvc/HttpHeaders.java index f47e39714..e31ddbf80 100644 --- a/docs/src/test/java/com/example/HttpHeaders.java +++ b/docs/src/test/java/com/example/mockmvc/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import org.springframework.test.web.servlet.MockMvc; @@ -32,19 +32,19 @@ public class HttpHeaders { public void headers() throws Exception { // tag::headers[] this.mockMvc - .perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // <1> - .andExpect(status().isOk()) - .andDo(document("headers", - requestHeaders( // <2> - headerWithName("Authorization").description( - "Basic auth credentials")), // <3> - responseHeaders( // <4> - headerWithName("X-RateLimit-Limit").description( - "The total number of requests permitted per period"), - headerWithName("X-RateLimit-Remaining").description( - "Remaining requests permitted in current period"), - headerWithName("X-RateLimit-Reset").description( - "Time at which the rate limit period will reset")))); + .perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // <1> + .andExpect(status().isOk()) + .andDo(document("headers", + requestHeaders( // <2> + headerWithName("Authorization").description( + "Basic auth credentials")), // <3> + responseHeaders( // <4> + headerWithName("X-RateLimit-Limit").description( + "The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining").description( + "Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset").description( + "Time at which the rate limit period will reset")))); // end::headers[] } } diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/mockmvc/Hypermedia.java similarity index 96% rename from docs/src/test/java/com/example/Hypermedia.java rename to docs/src/test/java/com/example/mockmvc/Hypermedia.java index 3cf6c1bcf..6358fa95e 100644 --- a/docs/src/test/java/com/example/Hypermedia.java +++ b/docs/src/test/java/com/example/mockmvc/Hypermedia.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; diff --git a/docs/src/test/java/com/example/InvokeService.java b/docs/src/test/java/com/example/mockmvc/InvokeService.java similarity index 93% rename from docs/src/test/java/com/example/InvokeService.java rename to docs/src/test/java/com/example/mockmvc/InvokeService.java index 097899f2b..2eb8eeb5c 100644 --- a/docs/src/test/java/com/example/InvokeService.java +++ b/docs/src/test/java/com/example/mockmvc/InvokeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; diff --git a/docs/src/test/java/com/example/AlwaysDo.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java similarity index 84% rename from docs/src/test/java/com/example/AlwaysDo.java rename to docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index efc64f6c7..ea6313cd1 100644 --- a/docs/src/test/java/com/example/AlwaysDo.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,10 +14,7 @@ * limitations under the License. */ -package com.example; - -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +package com.example.mockmvc; import org.junit.Before; import org.junit.Rule; @@ -26,24 +23,25 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -public class AlwaysDo { - +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +public class ParameterizedOutput { + @Rule public final RestDocumentation restDocumentation = new RestDocumentation("build"); private MockMvc mockMvc; private WebApplicationContext context; - - - // tag::always-do[] + // tag::parameterized-output[] @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(document("{method-name}/{step}/")) - .build(); + .alwaysDo(document("{method-name}/{step}/")).build(); } - // end::always-do[] + // end::parameterized-output[] + } diff --git a/docs/src/test/java/com/example/PathParameters.java b/docs/src/test/java/com/example/mockmvc/PathParameters.java similarity index 94% rename from docs/src/test/java/com/example/PathParameters.java rename to docs/src/test/java/com/example/mockmvc/PathParameters.java index c78105986..460d37dbc 100644 --- a/docs/src/test/java/com/example/PathParameters.java +++ b/docs/src/test/java/com/example/mockmvc/PathParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java similarity index 83% rename from docs/src/test/java/com/example/Payload.java rename to docs/src/test/java/com/example/mockmvc/Payload.java index 5ee1a8902..bed9663f1 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; @@ -51,7 +51,6 @@ public void explicitType() throws Exception { .andDo(document("index", responseFields( fieldWithPath("contact.email") .type(JsonFieldType.STRING) // <1> - .optional() .description("The user's email address")))); // end::explicit-type[] } @@ -61,16 +60,13 @@ public void constraints() throws Exception { .andExpect(status().isOk()) // tag::constraints[] .andDo(document("create-user", requestFields( - attributes( - key("title").value("Fields for user creation")), // <1> - fieldWithPath("name") - .description("The user's name") - .attributes( - key("constraints").value("Must not be null. Must not be empty")), // <2> - fieldWithPath("email") - .description("The user's email address") - .attributes( - key("constraints").value("Must be a valid email address"))))); // <3> + attributes(key("title").value("Fields for user creation")), // <1> + fieldWithPath("name").description("The user's name") + .attributes(key("constraints") + .value("Must not be null. Must not be empty")), // <2> + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints") + .value("Must be a valid email address"))))); // <3> // end::constraints[] } diff --git a/docs/src/test/java/com/example/PerTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java similarity index 90% rename from docs/src/test/java/com/example/PerTestPreprocessing.java rename to docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java index 504f96c09..60e56da18 100644 --- a/docs/src/test/java/com/example/PerTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/PerTestPreprocessing.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import org.springframework.test.web.servlet.MockMvc; @@ -33,8 +33,8 @@ public class PerTestPreprocessing { public void general() throws Exception { // tag::preprocessing[] this.mockMvc.perform(get("/")).andExpect(status().isOk()) - .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> - preprocessResponse(prettyPrint()))); // <2> + .andDo(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + preprocessResponse(prettyPrint()))); // <2> // end::preprocessing[] } diff --git a/docs/src/test/java/com/example/RequestParameters.java b/docs/src/test/java/com/example/mockmvc/RequestParameters.java similarity index 96% rename from docs/src/test/java/com/example/RequestParameters.java rename to docs/src/test/java/com/example/mockmvc/RequestParameters.java index 5a314958c..eece34115 100644 --- a/docs/src/test/java/com/example/RequestParameters.java +++ b/docs/src/test/java/com/example/mockmvc/RequestParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example; +package com.example.mockmvc; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; @@ -39,7 +39,7 @@ public void getQueryStringSnippet() throws Exception { ))); // end::request-parameters-query-string[] } - + public void postFormDataSnippet() throws Exception { // tag::request-parameters-form-data[] this.mockMvc.perform(post("/users").param("username", "Tester")) // <1> diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java new file mode 100644 index 000000000..4166c13e0 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class CustomDefaultSnippets { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + RequestSpecification spec; + + @Before + public void setUp() { + // tag::custom-default-snippets[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation) + .snippets().withDefaults(curlRequest())) + .build(); + // end::custom-default-snippets[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java new file mode 100644 index 000000000..eb85c8059 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class CustomEncoding { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + private RequestSpecification spec; + + @Before + public void setUp() { + // tag::custom-encoding[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation) + .snippets().withEncoding("ISO-8859-1")) + .build(); + // end::custom-encoding[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java new file mode 100644 index 000000000..68b8a88b1 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.restassured.RestDocumentationFilter; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class EveryTestPreprocessing { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation( + "target/generated-snippets"); + + // tag::setup[] + private RequestSpecification spec; + + private RestDocumentationFilter document; + + @Before + public void setup() { + this.document = document("{method-name}", + preprocessRequest(removeHeaders("Foo")), + preprocessResponse(prettyPrint())); // <1> + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(this.document)// <2> + .build(); + } + + // end::setup[] + + public void use() throws Exception { + // tag::use[] + this.document.snippets( // <1> + links(linkWithRel("self").description("Canonical self link"))); + RestAssured.given(this.spec) // <2> + .when().get("/") + .then().assertThat().statusCode(is(200)); + // end::use[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java new file mode 100644 index 000000000..c0943a777 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class ExampleApplicationTests { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation( + "build/generated-snippets"); + + // tag::setup[] + private RequestSpecification spec; + + @Before + public void setUp() { + this.spec = new RequestSpecBuilder().addFilter( + documentationConfiguration(this.restDocumentation)) // <1> + .build(); + } + // end::setup[] +} diff --git a/docs/src/test/java/com/example/restassured/HttpHeaders.java b/docs/src/test/java/com/example/restassured/HttpHeaders.java new file mode 100644 index 000000000..2556be02e --- /dev/null +++ b/docs/src/test/java/com/example/restassured/HttpHeaders.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class HttpHeaders { + + private RequestSpecification spec; + + public void headers() throws Exception { + // tag::headers[] + RestAssured.given(this.spec) + .filter(document("headers", + requestHeaders( // <1> + headerWithName("Authorization").description( + "Basic auth credentials")), // <2> + responseHeaders( // <3> + headerWithName("X-RateLimit-Limit").description( + "The total number of requests permitted per period"), + headerWithName("X-RateLimit-Remaining").description( + "Remaining requests permitted in current period"), + headerWithName("X-RateLimit-Reset").description( + "Time at which the rate limit period will reset")))) + .header("Authroization", "Basic dXNlcjpzZWNyZXQ=") // <4> + .when().get("/people") + .then().assertThat().statusCode(is(200)); + // end::headers[] + } +} diff --git a/docs/src/test/java/com/example/restassured/Hypermedia.java b/docs/src/test/java/com/example/restassured/Hypermedia.java new file mode 100644 index 000000000..2922377d8 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/Hypermedia.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.halLinks; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class Hypermedia { + + private RequestSpecification spec; + + public void defaultExtractor() throws Exception { + // tag::links[] + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("index", links( // <1> + linkWithRel("alpha").description("Link to the alpha resource"), // <2> + linkWithRel("bravo").description("Link to the bravo resource")))) // <3> + .get("/").then().assertThat().statusCode(is(200)); + // end::links[] + } + + public void explicitExtractor() throws Exception { + RestAssured.given(this.spec) + .accept("application/json") + // tag::explicit-extractor[] + .filter(document("index", links(halLinks(), // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))) + // end::explicit-extractor[] + .get("/").then().assertThat().statusCode(is(200)); + } + +} diff --git a/docs/src/test/java/com/example/restassured/InvokeService.java b/docs/src/test/java/com/example/restassured/InvokeService.java new file mode 100644 index 000000000..347c368ef --- /dev/null +++ b/docs/src/test/java/com/example/restassured/InvokeService.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class InvokeService { + + private RequestSpecification spec; + + public void invokeService() throws Exception { + // tag::invoke-service[] + RestAssured.given(this.spec) // <1> + .accept("application/json") // <2> + .filter(document("index")) // <3> + .when().get("/") // <4> + .then().assertThat().statusCode(is(200)); // <5> + // end::invoke-service[] + } +} diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java new file mode 100644 index 000000000..10fd8be1d --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class ParameterizedOutput { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation( + "build/generated-snippets"); + + private RequestSpecification spec; + + // tag::parameterized-output[] + @Before + public void setUp() { + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(document("{method-name}/{step}")).build(); + } + // end::parameterized-output[] + +} diff --git a/docs/src/test/java/com/example/restassured/PathParameters.java b/docs/src/test/java/com/example/restassured/PathParameters.java new file mode 100644 index 000000000..81c7cc3e4 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/PathParameters.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class PathParameters { + + private RequestSpecification spec; + + public void pathParametersSnippet() throws Exception { + // tag::path-parameters[] + RestAssured.given(this.spec) + .filter(document("locations", pathParameters( // <1> + parameterWithName("latitude").description("The location's latitude"), // <2> + parameterWithName("longitude").description("The location's longitude")))) // <3> + .when().get("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // <4> + .then().assertThat().statusCode(is(200)); + // end::path-parameters[] + } +} diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java new file mode 100644 index 000000000..ee6c2456f --- /dev/null +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -0,0 +1,75 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.springframework.restdocs.payload.JsonFieldType; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +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.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +public class Payload { + + private RequestSpecification spec; + + public void response() throws Exception { + // tag::response[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("user", responseFields( // <1> + fieldWithPath("contact").description("The user's contact details"), // <2> + fieldWithPath("contact.email").description("The user's email address")))) // <3> + .when().get("/user/5") + .then().assertThat().statusCode(is(200)); + // end::response[] + } + + public void explicitType() throws Exception { + RestAssured.given(this.spec).accept("application/json") + // tag::explicit-type[] + .filter(document("user", responseFields( + fieldWithPath("contact.email") + .type(JsonFieldType.STRING) // <1> + .description("The user's email address")))) + // end::explicit-type[] + .when().get("/user/5") + .then().assertThat().statusCode(is(200)); + } + + public void constraints() throws Exception { + RestAssured.given(this.spec).accept("application/json") + // tag::constraints[] + .filter(document("create-user", requestFields( + attributes(key("title").value("Fields for user creation")), // <1> + fieldWithPath("name").description("The user's name") + .attributes(key("constraints") + .value("Must not be null. Must not be empty")), // <2> + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints") + .value("Must be a valid email address"))))) // <3> + // end::constraints[] + .when().post("/users") + .then().assertThat().statusCode(is(200)); + } + +} diff --git a/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java new file mode 100644 index 000000000..69f4936ef --- /dev/null +++ b/docs/src/test/java/com/example/restassured/PerTestPreprocessing.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class PerTestPreprocessing { + + private RequestSpecification spec; + + public void general() throws Exception { + // tag::preprocessing[] + RestAssured.given(this.spec) + .filter(document("index", preprocessRequest(removeHeaders("Foo")), // <1> + preprocessResponse(prettyPrint()))) // <2> + .when().get("/") + .then().assertThat().statusCode(is(200)); + // end::preprocessing[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestParameters.java b/docs/src/test/java/com/example/restassured/RequestParameters.java new file mode 100644 index 000000000..86f40c4be --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestParameters.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestParameters { + + private RequestSpecification spec; + + public void getQueryStringSnippet() throws Exception { + // tag::request-parameters-query-string[] + RestAssured.given(this.spec) + .filter(document("users", requestParameters( // <1> + parameterWithName("page").description("The page to retrieve"), // <2> + parameterWithName("per_page").description("Entries per page")))) // <3> + .when().get("/users?page=2&per_page=100") // <4> + .then().assertThat().statusCode(is(200)); + // end::request-parameters-query-string[] + } + + public void postFormDataSnippet() throws Exception { + // tag::request-parameters-form-data[] + RestAssured.given(this.spec) + .filter(document("create-user", requestParameters( + parameterWithName("username").description("The user's username")))) + .formParam("username", "Tester") // <1> + .when().post("/users") // <2> + .then().assertThat().statusCode(is(200)); + // end::request-parameters-form-data[] + } + +} diff --git a/settings.gradle b/settings.gradle index 7552e239c..8d6619a19 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,5 @@ rootProject.name = 'spring-restdocs' include 'docs' include 'spring-restdocs-core' -include 'spring-restdocs-mockmvc' \ No newline at end of file +include 'spring-restdocs-mockmvc' +include 'spring-restdocs-restassured' \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java similarity index 59% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java index f8effd0a9..7a8a9e03e 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,9 +14,11 @@ * limitations under the License. */ -package org.springframework.restdocs.mockmvc; +package org.springframework.restdocs.config; -import org.springframework.mock.web.MockHttpServletRequest; +import java.util.Map; + +import org.springframework.restdocs.RestDocumentationContext; /** * Abstract configurer that declares methods that are internal to the documentation @@ -24,13 +26,15 @@ * * @author Andy Wilkinson */ -abstract class AbstractConfigurer { +public abstract class AbstractConfigurer { /** - * Applies the configuration, possibly by modifying the given {@code request}. + * Applies the configurer to the given {@code configuration}. * - * @param request the request that may be modified + * @param configuration the configuration to be configured + * @param context the current documentation context */ - abstract void apply(MockHttpServletRequest request); + public abstract void apply(Map configuration, + RestDocumentationContext context); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java new file mode 100644 index 000000000..1463edde4 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2016 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.config; + +/** + * Base class for {@link NestedConfigurer} implementations. + * + * @param

    The type of the configurer's parent + * @author Andy Wilkinson + */ +public abstract class AbstractNestedConfigurer

    extends AbstractConfigurer implements + NestedConfigurer

    { + + private final P parent; + + /** + * Creates a new {@code AbstractNestedConfigurer} with the given {@code parent}. + * @param parent the parent + */ + protected AbstractNestedConfigurer(P parent) { + this.parent = parent; + } + + @Override + public final P and() { + return this.parent; + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java similarity index 72% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java index 1c6768c8f..72e2f41bf 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/NestedConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,22 +14,20 @@ * limitations under the License. */ -package org.springframework.restdocs.mockmvc; - -import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +package org.springframework.restdocs.config; /** * A configurer that is nested and, therefore, has a parent. * - * @param The parent's type + * @param

    The parent's type * @author Andy Wilkinson */ -interface NestedConfigurer { +interface NestedConfigurer

    { /** * Returns the configurer's parent. * * @return the parent */ - PARENT and(); + P and(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java new file mode 100644 index 000000000..bedec85ae --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -0,0 +1,132 @@ +/* + * Copyright 2014-2016 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.config; + +import java.util.Map; + +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.StandardWriterResolver; +import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.templates.StandardTemplateResourceResolver; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +/** + * Abstract base class for the configuration of Spring REST Docs. + * + * @param The concrete type of the {@link SnippetConfigurer}. + * @param The concrete type of this configurer, to be returned from methods that + * support chaining + * @author Andy Wilkinson + */ +public abstract class RestDocumentationConfigurer { + + private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); + + private final TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); + + /** + * Returns a {@link SnippetConfigurer} that can be used to configure the snippets that + * will be generated. + * + * @return the snippet configurer + */ + public abstract S snippets(); + + /** + * Configures the {@link TemplateEngine} that will be used for snippet rendering. + * + * @param templateEngine the template engine to use + * @return {@code this} + */ + @SuppressWarnings("unchecked") + public final T templateEngine(TemplateEngine templateEngine) { + this.templateEngineConfigurer.setTemplateEngine(templateEngine); + return (T) 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} + */ + @SuppressWarnings("unchecked") + public final T writerResolver(WriterResolver writerResolver) { + this.writerResolverConfigurer.setWriterResolver(writerResolver); + return (T) this; + } + + /** + * Returns the configurer used to configure the context with a {@link WriterResolver}. + * + * @return the configurer + */ + protected final AbstractConfigurer getWriterResolverConfigurer() { + return this.writerResolverConfigurer; + } + + /** + * Returns the configurer used to configure the context with a {@link TemplateEngine}. + * + * @return the configurer + */ + protected final AbstractConfigurer getTemplateEngineConfigurer() { + return this.templateEngineConfigurer; + } + + private static final class TemplateEngineConfigurer extends AbstractConfigurer { + + private TemplateEngine templateEngine = new MustacheTemplateEngine( + new StandardTemplateResourceResolver()); + + @Override + public void apply(Map configuration, + RestDocumentationContext context) { + configuration.put(TemplateEngine.class.getName(), this.templateEngine); + } + + private void setTemplateEngine(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + } + + private static final class WriterResolverConfigurer extends AbstractConfigurer { + + private WriterResolver writerResolver; + + @Override + public void apply(Map configuration, + RestDocumentationContext context) { + WriterResolver resolverToUse = this.writerResolver; + if (resolverToUse == null) { + resolverToUse = new StandardWriterResolver( + new RestDocumentationContextPlaceholderResolver(context)); + } + configuration.put(WriterResolver.class.getName(), resolverToUse); + } + + private void setWriterResolver(WriterResolver writerResolver) { + this.writerResolver = writerResolver; + } + + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java similarity index 61% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 2723d0166..6f4be5a54 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,12 +14,13 @@ * limitations under the License. */ -package org.springframework.restdocs.mockmvc; +package org.springframework.restdocs.config; import java.util.Arrays; import java.util.List; +import java.util.Map; -import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.curl.CurlDocumentation; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; @@ -28,10 +29,16 @@ /** * A configurer that can be used to configure the generated documentation snippets. * + * @param

    The type of the configurer's parent + * @param The concrete type of the configurer to be returned from chained methods * @author Andy Wilkinson */ -public class SnippetConfigurer extends - AbstractNestedConfigurer { +public abstract class SnippetConfigurer extends AbstractNestedConfigurer

    { + + /** + * The name of the attribute that is used to hold the default snippets. + */ + public static final String ATTRIBUTE_DEFAULT_SNIPPETS = "org.springframework.restdocs.defaultSnippets"; private List defaultSnippets = Arrays.asList( CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(), @@ -46,10 +53,22 @@ public class SnippetConfigurer extends private String snippetEncoding = DEFAULT_SNIPPET_ENCODING; - SnippetConfigurer(RestDocumentationMockMvcConfigurer parent) { + /** + * Creates a new {@code SnippetConfigurer} with the given {@code parent}. + * + * @param parent the parent + */ + protected SnippetConfigurer(P parent) { super(parent); } + @Override + public void apply(Map configuration, RestDocumentationContext context) { + ((WriterResolver) configuration.get(WriterResolver.class.getName())) + .setEncoding(this.snippetEncoding); + configuration.put(ATTRIBUTE_DEFAULT_SNIPPETS, this.defaultSnippets); + } + /** * Configures any documentation snippets to be written using the given * {@code encoding}. The default is UTF-8. @@ -57,17 +76,10 @@ public class SnippetConfigurer extends * @param encoding the encoding * @return {@code this} */ - public SnippetConfigurer withEncoding(String encoding) { + @SuppressWarnings("unchecked") + public T withEncoding(String encoding) { this.snippetEncoding = encoding; - return this; - } - - @Override - void apply(MockHttpServletRequest request) { - ((WriterResolver) request.getAttribute(WriterResolver.class.getName())) - .setEncoding(this.snippetEncoding); - request.setAttribute("org.springframework.restdocs.mockmvc.defaultSnippets", - this.defaultSnippets); + return (T) this; } /** @@ -76,8 +88,9 @@ void apply(MockHttpServletRequest request) { * @param defaultSnippets the default snippets * @return {@code this} */ - public SnippetConfigurer withDefaults(Snippet... defaultSnippets) { + @SuppressWarnings("unchecked") + public T withDefaults(Snippet... defaultSnippets) { this.defaultSnippets = Arrays.asList(defaultSnippets); - return this; + return (T) this; } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 5df99f6f6..0670be1da 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -92,8 +92,9 @@ protected Set extractActualParameters(Operation operation) { private String extractUrlTemplate(Operation operation) { String urlTemplate = (String) operation.getAttributes().get( "org.springframework.restdocs.urlTemplate"); - Assert.notNull(urlTemplate, - "urlTemplate not found. Did you use RestDocumentationRequestBuilders to " + Assert.notNull( + urlTemplate, + "urlTemplate not found. If you are using MockMvc, did you use RestDocumentationRequestBuilders to " + "build the request?"); return urlTemplate; } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index bedd3753a..541f380d9 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -44,9 +44,9 @@ private MockMvcRestDocumentation() { * @return the configurer * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) */ - public static RestDocumentationMockMvcConfigurer documentationConfiguration( + public static MockMvcRestDocumentationConfigurer documentationConfiguration( RestDocumentation restDocumentation) { - return new RestDocumentationMockMvcConfigurer(restDocumentation); + return new MockMvcRestDocumentationConfigurer(restDocumentation); } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java new file mode 100644 index 000000000..f9beacace --- /dev/null +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-2016 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.mockmvc; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.AbstractConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.web.context.WebApplicationContext; + +/** + * A MockMvc-specific {@link RestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +public class MockMvcRestDocumentationConfigurer + extends + RestDocumentationConfigurer + implements MockMvcConfigurer { + + private final MockMvcSnippetConfigurer snippetConfigurer = new MockMvcSnippetConfigurer( + this); + + private final UriConfigurer uriConfigurer = new UriConfigurer(this); + + private final RestDocumentation restDocumentation; + + MockMvcRestDocumentationConfigurer(RestDocumentation restDocumentation) { + super(); + this.restDocumentation = restDocumentation; + } + + /** + * 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; + } + + @Override + public RequestPostProcessor beforeMockMvcCreated( + ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation, + Arrays.asList(getTemplateEngineConfigurer(), + getWriterResolverConfigurer(), snippets(), this.uriConfigurer)); + } + + @Override + public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { + // Nothing to do + } + + @Override + public MockMvcSnippetConfigurer snippets() { + return this.snippetConfigurer; + } + + private static final class ConfigurerApplyingRequestPostProcessor implements + RequestPostProcessor { + + private final RestDocumentation restDocumentation; + + private final List configurers; + + private ConfigurerApplyingRequestPostProcessor( + RestDocumentation restDocumentation, List configurers) { + this.restDocumentation = restDocumentation; + this.configurers = configurers; + } + + @Override + public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { + RestDocumentationContext context = this.restDocumentation.beforeOperation(); + request.setAttribute(RestDocumentationContext.class.getName(), context); + Map configuration = new HashMap<>(); + configuration.put(MockHttpServletRequest.class.getName(), request); + String urlTemplateAttribute = "org.springframework.restdocs.urlTemplate"; + configuration.put(urlTemplateAttribute, + request.getAttribute(urlTemplateAttribute)); + request.setAttribute("org.springframework.restdocs.configuration", + configuration); + for (AbstractConfigurer configurer : this.configurers) { + configurer.apply(configuration, context); + } + return request; + } + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java similarity index 65% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java index a4444e7cb..a46302c01 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/AbstractNestedConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,40 +16,33 @@ package org.springframework.restdocs.mockmvc; +import org.springframework.restdocs.config.SnippetConfigurer; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.web.context.WebApplicationContext; /** - * Base class for {@link NestedConfigurer} implementations. + * A configurer that can be used to configure the generated documentation snippets. * - * @param The type of the configurer's parent * @author Andy Wilkinson */ -abstract class AbstractNestedConfigurer extends - AbstractConfigurer implements NestedConfigurer, MockMvcConfigurer { +public class MockMvcSnippetConfigurer extends + SnippetConfigurer + implements MockMvcConfigurer { - private final PARENT parent; - - protected AbstractNestedConfigurer(PARENT parent) { - this.parent = parent; - } - - @Override - public PARENT and() { - return this.parent; + MockMvcSnippetConfigurer(MockMvcRestDocumentationConfigurer parent) { + super(parent); } @Override public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { - this.parent.afterConfigurerAdded(builder); + and().afterConfigurerAdded(builder); } @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return this.parent.beforeMockMvcCreated(builder, context); + return and().beforeMockMvcCreated(builder, context); } - } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java deleted file mode 100644 index 20f06771c..000000000 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java +++ /dev/null @@ -1,198 +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.mockmvc; - -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.curl.CurlDocumentation; -import org.springframework.restdocs.http.HttpDocumentation; -import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; -import org.springframework.restdocs.snippet.StandardWriterResolver; -import org.springframework.restdocs.snippet.WriterResolver; -import org.springframework.restdocs.templates.StandardTemplateResourceResolver; -import org.springframework.restdocs.templates.TemplateEngine; -import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.test.web.servlet.request.RequestPostProcessor; -import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; -import org.springframework.test.web.servlet.setup.MockMvcConfigurer; -import org.springframework.test.web.servlet.setup.MockMvcConfigurerAdapter; -import org.springframework.web.context.WebApplicationContext; - -/** - * A {@link MockMvcConfigurer} that can be used to configure the documentation. - *

    - * In the absence of any {@link #snippets() customization} the following snippets will be - * produced by default: - *

      - *
    • {@link CurlDocumentation#curlRequest() Curl request}
    • - *
    • {@link HttpDocumentation#httpRequest() HTTP request}
    • - *
    • {@link HttpDocumentation#httpResponse() HTTP response}
    • - *
    - *

    - * In the absence of any {@link #uris() customization}, documented URIs have the following - * defaults: - *

      - *
    • Scheme: {@code http}
    • - *
    • Host: {@code localhost}
    • - *
    • Port: {@code 8080}
    • - *
    - * - * @author Andy Wilkinson - * @author Dmitriy Mayboroda - * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) - * @see MockMvcRestDocumentation#documentationConfiguration(RestDocumentation) - */ -public class RestDocumentationMockMvcConfigurer extends MockMvcConfigurerAdapter { - - private final UriConfigurer uriConfigurer = new UriConfigurer(this); - - private final SnippetConfigurer snippetConfigurer = new SnippetConfigurer(this); - - private final RequestPostProcessor requestPostProcessor; - - private final TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); - - private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); - - /** - * Creates a new {code RestDocumentationMockMvcConfigurer} that will use the given - * {@code restDocumentation} when configuring MockMvc. - * - * @param restDocumentation the rest documentation - * @see MockMvcRestDocumentation#documentationConfiguration(RestDocumentation) - */ - RestDocumentationMockMvcConfigurer(RestDocumentation restDocumentation) { - this.requestPostProcessor = new ConfigurerApplyingRequestPostProcessor( - restDocumentation, this.uriConfigurer, this.writerResolverConfigurer, - this.snippetConfigurer, 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 RestDocumentationMockMvcConfigurer 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 RestDocumentationMockMvcConfigurer writerResolver(WriterResolver writerResolver) { - this.writerResolverConfigurer.setWriterResolver(writerResolver); - return this; - } - - @Override - public RequestPostProcessor beforeMockMvcCreated( - ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return this.requestPostProcessor; - } - - private static final class TemplateEngineConfigurer extends AbstractConfigurer { - - private TemplateEngine templateEngine = new MustacheTemplateEngine( - new StandardTemplateResourceResolver()); - - @Override - void apply(MockHttpServletRequest request) { - request.setAttribute(TemplateEngine.class.getName(), this.templateEngine); - } - - void setTemplateEngine(TemplateEngine templateEngine) { - this.templateEngine = templateEngine; - } - - } - - private static final class WriterResolverConfigurer extends AbstractConfigurer { - - private WriterResolver writerResolver; - - @Override - void apply(MockHttpServletRequest request) { - WriterResolver resolverToUse = this.writerResolver; - if (resolverToUse == null) { - resolverToUse = new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver( - (RestDocumentationContext) request - .getAttribute(RestDocumentationContext.class - .getName()))); - } - request.setAttribute(WriterResolver.class.getName(), resolverToUse); - } - - void setWriterResolver(WriterResolver writerResolver) { - this.writerResolver = writerResolver; - } - - } - - private static final class ConfigurerApplyingRequestPostProcessor implements - RequestPostProcessor { - - private final RestDocumentation restDocumentation; - - private final AbstractConfigurer[] configurers; - - private ConfigurerApplyingRequestPostProcessor( - RestDocumentation restDocumentation, AbstractConfigurer... configurers) { - this.restDocumentation = restDocumentation; - this.configurers = configurers; - } - - @Override - public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - request.setAttribute(RestDocumentationContext.class.getName(), - this.restDocumentation.beforeOperation()); - for (AbstractConfigurer configurer : this.configurers) { - configurer.apply(request); - } - return request; - } - - } - -} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 425cd2fc0..075c162fb 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.SnippetConfigurer; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationResponse; @@ -33,8 +35,6 @@ import org.springframework.test.web.servlet.ResultHandler; import org.springframework.util.Assert; -import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; - /** * A Spring MVC Test {@code ResultHandler} for documenting RESTful APIs. * @@ -85,9 +85,15 @@ public class RestDocumentationResultHandler implements ResultHandler { @Override public void handle(MvcResult result) throws Exception { Map attributes = new HashMap<>(); - for (String name : iterable(result.getRequest().getAttributeNames())) { - attributes.put(name, result.getRequest().getAttribute(name)); - } + attributes.put(RestDocumentationContext.class.getName(), result.getRequest() + .getAttribute(RestDocumentationContext.class.getName())); + attributes.put("org.springframework.restdocs.urlTemplate", result.getRequest() + .getAttribute("org.springframework.restdocs.urlTemplate")); + @SuppressWarnings("unchecked") + Map configuration = (Map) result.getRequest() + .getAttribute("org.springframework.restdocs.configuration"); + attributes.putAll(configuration); + OperationRequest request = this.requestPreprocessor .preprocess(new MockMvcOperationRequestFactory() .createOperationRequest(result.getRequest())); @@ -103,7 +109,7 @@ public void handle(MvcResult result) throws Exception { } /** - * Adds the given {@code snippets} such that that are documented when this result + * Adds the given {@code snippets} such that they are documented when this result * handler is called. * * @param snippets the snippets to add @@ -116,9 +122,10 @@ public RestDocumentationResultHandler snippets(Snippet... snippets) { @SuppressWarnings("unchecked") private List getSnippets(MvcResult result) { - List combinedSnippets = new ArrayList<>((List) result - .getRequest().getAttribute( - "org.springframework.restdocs.mockmvc.defaultSnippets")); + List combinedSnippets = new ArrayList<>( + (List) ((Map) result.getRequest().getAttribute( + "org.springframework.restdocs.configuration")) + .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS)); combinedSnippets.addAll(this.snippets); return combinedSnippets; } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java index b7024fd4c..b2c3a8c81 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,7 +16,15 @@ package org.springframework.restdocs.mockmvc; +import java.util.Map; + import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.AbstractNestedConfigurer; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; +import org.springframework.web.context.WebApplicationContext; /** * A configurer that can be used to configure the documented URIs. @@ -24,7 +32,8 @@ * @author Andy Wilkinson */ public class UriConfigurer extends - AbstractNestedConfigurer { + AbstractNestedConfigurer implements + MockMvcConfigurer { /** * The default scheme for documented URIs. @@ -53,7 +62,7 @@ public class UriConfigurer extends private int port = DEFAULT_PORT; - UriConfigurer(RestDocumentationMockMvcConfigurer parent) { + UriConfigurer(MockMvcRestDocumentationConfigurer parent) { super(parent); } @@ -94,10 +103,23 @@ public UriConfigurer withPort(int port) { } @Override - void apply(MockHttpServletRequest request) { + public void apply(Map configuration, RestDocumentationContext context) { + MockHttpServletRequest request = (MockHttpServletRequest) configuration + .get(MockHttpServletRequest.class.getName()); request.setScheme(this.scheme); request.setServerPort(this.port); request.setServerName(this.host); } + @Override + public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { + and().afterConfigurerAdded(builder); + } + + @Override + public RequestPostProcessor beforeMockMvcCreated( + ConfigurableMockMvcBuilder builder, WebApplicationContext context) { + return and().beforeMockMvcCreated(builder, context); + } + } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java similarity index 85% rename from spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java rename to spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java index 486bfd8e6..ad6ac2691 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -33,12 +33,12 @@ import static org.junit.Assert.assertThat; /** - * Tests for {@link RestDocumentationMockMvcConfigurer}. + * Tests for {@link MockMvcRestDocumentationConfigurer}. * * @author Andy Wilkinson * @author Dmitriy Mayboroda */ -public class RestDocumentationConfigurerTests { +public class MockMvcRestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); @@ -47,7 +47,7 @@ public class RestDocumentationConfigurerTests { @Test public void defaultConfiguration() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -56,7 +56,7 @@ public void defaultConfiguration() { @Test public void customScheme() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).uris().withScheme("https") .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -66,7 +66,7 @@ public void customScheme() { @Test public void customHost() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).uris().withHost("api.example.com") .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -76,7 +76,7 @@ public void customHost() { @Test public void customPort() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).uris().withPort(8081) .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); @@ -86,7 +86,7 @@ public void customPort() { @Test public void noContentLengthHeaderWhenRequestHasNotContent() { - RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( + RequestPostProcessor postProcessor = new MockMvcRestDocumentationConfigurer( this.restDocumentation).uris().withPort(8081) .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 76e3dd677..d444dd177 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -83,7 +83,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * Integration tests for using Spring REST Docs with Spring Test's Mock MVC. + * Integration tests for using Spring REST Docs with Spring Test's MockMvc. * * @author Andy Wilkinson * @author Dewet Diener @@ -112,8 +112,10 @@ public void clearOutputDirSystemProperty() { @Test public void basicSnippetGeneration() throws Exception { - MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)).build(); + MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation) + .snippets().withEncoding("UTF-8")).build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("basic")); diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle new file mode 100644 index 000000000..e7c33f79c --- /dev/null +++ b/spring-restdocs-restassured/build.gradle @@ -0,0 +1,16 @@ +dependencies { + compile project(':spring-restdocs-core') + compile 'com.jayway.restassured:rest-assured' + + testCompile 'org.mockito:mockito-core' + testCompile 'org.hamcrest:hamcrest-library' + testCompile 'org.springframework.hateoas:spring-hateoas' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.2.5.RELEASE' + testCompile 'org.springframework:spring-test' + testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') + testRuntime 'commons-logging:commons-logging:1.2' +} + +test { + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" +} \ No newline at end of file diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java new file mode 100644 index 000000000..c3ef0b20c --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014-2016 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.restassured; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; +import org.springframework.restdocs.operation.Parameters; + +import com.jayway.restassured.response.Header; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.MultiPartSpecification; + +/** + * A factory for creating an {@link OperationRequest} derived from a REST Assured + * {@link FilterableRequestSpecification}. + * + * @author Andy Wilkinson + */ +class RestAssuredOperationRequestFactory { + + OperationRequest createOperationRequest(FilterableRequestSpecification requestSpec) { + return new OperationRequestFactory().create(URI.create(requestSpec.getURI()), + HttpMethod.valueOf(requestSpec.getMethod().name()), + extractContent(requestSpec), extractHeaders(requestSpec), + extractParameters(requestSpec), extractParts(requestSpec)); + } + + private byte[] extractContent(FilterableRequestSpecification requestSpec) { + return convertContent(requestSpec.getBody()); + } + + private byte[] convertContent(Object content) { + if (content instanceof String) { + return ((String) content).getBytes(); + } + else if (content instanceof byte[]) { + return (byte[]) content; + } + else if (content == null) { + return new byte[0]; + } + else { + throw new IllegalStateException("Unsupported request content: " + + content.getClass().getName()); + } + } + + private HttpHeaders extractHeaders(FilterableRequestSpecification requestSpec) { + HttpHeaders httpHeaders = new HttpHeaders(); + for (Header header : requestSpec.getHeaders()) { + httpHeaders.add(header.getName(), header.getValue()); + } + return httpHeaders; + } + + private Parameters extractParameters(FilterableRequestSpecification requestSpec) { + Parameters parameters = new Parameters(); + for (Entry entry : requestSpec.getQueryParams().entrySet()) { + parameters.add(entry.getKey(), entry.getValue().toString()); + } + for (Entry entry : requestSpec.getRequestParams().entrySet()) { + parameters.add(entry.getKey(), entry.getValue().toString()); + } + for (Entry entry : requestSpec.getFormParams().entrySet()) { + parameters.add(entry.getKey(), entry.getValue().toString()); + } + return parameters; + } + + private Collection extractParts( + FilterableRequestSpecification requestSpec) { + List parts = new ArrayList<>(); + for (MultiPartSpecification multiPartSpec : requestSpec.getMultiPartParams()) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(multiPartSpec.getMimeType() == null ? MediaType.TEXT_PLAIN + : MediaType.parseMediaType(multiPartSpec.getMimeType())); + parts.add(new OperationRequestPartFactory().create( + multiPartSpec.getControlName(), multiPartSpec.getFileName(), + convertContent(multiPartSpec.getContent()), headers)); + } + return parts; + } +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java new file mode 100644 index 000000000..bbcd251ee --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014-2016 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.restassured; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; + +import com.jayway.restassured.response.Header; +import com.jayway.restassured.response.Response; + +/** + * A factory for creating an {@link OperationResponse} derived from a REST Assured + * {@link Response}. + * + * @author Andy Wilkinson + */ +class RestAssuredOperationResponseFactory { + + OperationResponse createOperationResponse(Response response) { + return new OperationResponseFactory().create( + HttpStatus.valueOf(response.getStatusCode()), extractHeaders(response), + extractContent(response)); + } + + private HttpHeaders extractHeaders(Response response) { + HttpHeaders httpHeaders = new HttpHeaders(); + for (Header header : response.getHeaders()) { + httpHeaders.add(header.getName(), header.getValue()); + } + return httpHeaders; + } + + private byte[] extractContent(Response response) { + return response.getBody().asByteArray(); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java new file mode 100644 index 000000000..447ba1f55 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014-2016 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.restassured; + +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting RESTful APIs using REST Assured. + * + * @author Andy Wilkinson + */ +public abstract class RestAssuredRestDocumentation { + + private RestAssuredRestDocumentation() { + + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets}. + * + * @param identifier an identifier for the API call that is being documented + * @param snippets the snippets that will document the API call + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, Snippet... snippets) { + return new RestDocumentationFilter(identifier, snippets); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} is applied to the request before it is documented. + * + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param snippets the snippets + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, + OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { + return new RestDocumentationFilter(identifier, requestPreprocessor, snippets); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code responsePreprocessor} is applied to the request before it is documented. + * + * @param identifier an identifier for the API call that is being documented + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + return new RestDocumentationFilter(identifier, responsePreprocessor, snippets); + } + + /** + * Documents the API call with the given {@code identifier} using the given + * {@code snippets} in addition to any default snippets. The given + * {@code requestPreprocessor} and {@code responsePreprocessor} are applied to the + * request and response respectively before they are documented. + * + * @param identifier an identifier for the API call that is being documented + * @param requestPreprocessor the request preprocessor + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + * @return a {@link RestDocumentationFilter} that will produce the documentation + */ + public static RestDocumentationFilter document(String identifier, + OperationRequestPreprocessor requestPreprocessor, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + return new RestDocumentationFilter(identifier, requestPreprocessor, + responsePreprocessor, snippets); + } + + /** + * Provides access to a {@link RestAssuredRestDocumentationConfigurer} that can be + * used to configure Spring REST Docs using the given {@code restDocumentation}. + * + * @param restDocumentation the REST documentation + * @return the configurer + */ + public static RestAssuredRestDocumentationConfigurer documentationConfiguration( + RestDocumentation restDocumentation) { + return new RestAssuredRestDocumentationConfigurer(restDocumentation); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java new file mode 100644 index 000000000..5dbdb1c09 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2014-2016 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.restassured; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.AbstractConfigurer; +import org.springframework.restdocs.config.RestDocumentationConfigurer; + +import com.jayway.restassured.filter.Filter; +import com.jayway.restassured.filter.FilterContext; +import com.jayway.restassured.response.Response; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.FilterableResponseSpecification; + +/** + * A REST Assured-specific {@link RestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +public final class RestAssuredRestDocumentationConfigurer + extends + RestDocumentationConfigurer + implements Filter { + + private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer( + this); + + private final List configurers; + + private final RestDocumentation restDocumentation; + + RestAssuredRestDocumentationConfigurer(RestDocumentation restDocumentation) { + this.restDocumentation = restDocumentation; + this.configurers = Arrays.asList(getTemplateEngineConfigurer(), + getWriterResolverConfigurer(), snippets()); + } + + @Override + public RestAssuredSnippetConfigurer snippets() { + return this.snippetConfigurer; + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, + FilterableResponseSpecification responseSpec, FilterContext filterContext) { + RestDocumentationContext context = this.restDocumentation.beforeOperation(); + filterContext.setValue(RestDocumentationContext.class.getName(), context); + Map configuration = new HashMap<>(); + filterContext.setValue("org.springframework.restdocs.configuration", + configuration); + for (AbstractConfigurer configurer : this.configurers) { + configurer.apply(configuration, context); + } + return filterContext.next(requestSpec, responseSpec); + } +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java new file mode 100644 index 000000000..d91b38a18 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2016 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.restassured; + +import org.springframework.restdocs.config.SnippetConfigurer; + +import com.jayway.restassured.filter.Filter; +import com.jayway.restassured.filter.FilterContext; +import com.jayway.restassured.response.Response; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.FilterableResponseSpecification; + +/** + * A configurer that can be used to configure the generated documentation snippets when + * using REST Assured. + * + * @author Andy Wilkinson + */ +public final class RestAssuredSnippetConfigurer + extends + SnippetConfigurer + implements Filter { + + RestAssuredSnippetConfigurer(RestAssuredRestDocumentationConfigurer parent) { + super(parent); + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, + FilterableResponseSpecification responseSpec, FilterContext context) { + return and().filter(requestSpec, responseSpec, context); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java new file mode 100644 index 000000000..3460a99a3 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -0,0 +1,164 @@ +/* + * Copyright 2014-2016 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.restassured; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.config.SnippetConfigurer; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.StandardOperation; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.snippet.Snippet; + +import com.jayway.restassured.filter.Filter; +import com.jayway.restassured.filter.FilterContext; +import com.jayway.restassured.response.Response; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.FilterableResponseSpecification; + +/** + * A REST Assured {@link Filter} for documenting RESTful APIs. + * + * @author Andy Wilkinson + */ +public final class RestDocumentationFilter implements Filter { + + private final String identifier; + + private final OperationRequestPreprocessor requestPreprocessor; + + private final OperationResponsePreprocessor responsePreprocessor; + + private final List snippets; + + RestDocumentationFilter(String identifier, Snippet... snippets) { + this(identifier, new IdentityOperationRequestPreprocessor(), + new IdentityOperationResponsePreprocessor(), snippets); + } + + RestDocumentationFilter(String identifier, + OperationRequestPreprocessor operationRequestPreprocessor, + Snippet... snippets) { + this(identifier, operationRequestPreprocessor, + new IdentityOperationResponsePreprocessor(), snippets); + } + + RestDocumentationFilter(String identifier, + OperationResponsePreprocessor operationResponsePreprocessor, + Snippet... snippets) { + this(identifier, new IdentityOperationRequestPreprocessor(), + operationResponsePreprocessor, snippets); + } + + RestDocumentationFilter(String identifier, + OperationRequestPreprocessor requestPreprocessor, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + this.identifier = identifier; + this.requestPreprocessor = requestPreprocessor; + this.responsePreprocessor = responsePreprocessor; + this.snippets = new ArrayList<>(Arrays.asList(snippets)); + } + + @Override + public Response filter(FilterableRequestSpecification requestSpec, + FilterableResponseSpecification responseSpec, FilterContext context) { + Response response = context.next(requestSpec, responseSpec); + + OperationRequest operationRequest = this.requestPreprocessor + .preprocess(new RestAssuredOperationRequestFactory() + .createOperationRequest(requestSpec)); + OperationResponse operationResponse = this.responsePreprocessor + .preprocess(new RestAssuredOperationResponseFactory() + .createOperationResponse(response)); + + RestDocumentationContext documentationContext = context + .getValue(RestDocumentationContext.class.getName()); + + Map attributes = new HashMap<>(); + attributes.put(RestDocumentationContext.class.getName(), documentationContext); + attributes.put("org.springframework.restdocs.urlTemplate", + requestSpec.getUserDefinedPath()); + Map configuration = context + .getValue("org.springframework.restdocs.configuration"); + attributes.putAll(configuration); + + Operation operation = new StandardOperation(this.identifier, operationRequest, + operationResponse, attributes); + + try { + for (Snippet snippet : getSnippets(configuration)) { + snippet.document(operation); + } + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + + return response; + } + + /** + * Adds the given {@code snippets} such that they are documented when this result + * handler is called. + * + * @param snippets the snippets to add + * @return this {@code RestDocumentationFilter} + */ + public RestDocumentationFilter snippets(Snippet... snippets) { + this.snippets.addAll(Arrays.asList(snippets)); + return this; + } + + @SuppressWarnings("unchecked") + private List getSnippets(Map configuration) { + List combinedSnippets = new ArrayList<>( + (List) configuration + .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS)); + combinedSnippets.addAll(this.snippets); + return combinedSnippets; + } + + private static final class IdentityOperationRequestPreprocessor implements + OperationRequestPreprocessor { + + @Override + public OperationRequest preprocess(OperationRequest request) { + return request; + } + + } + + private static final class IdentityOperationResponsePreprocessor implements + OperationResponsePreprocessor { + + @Override + public OperationResponse preprocess(OperationResponse response) { + return response; + } + + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java new file mode 100644 index 000000000..5e5c1f9f9 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2016 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.restassured.operation.preprocess; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; + +/** + * Static factory methods for creating + * {@link org.springframework.restdocs.operation.preprocess.OperationPreprocessor + * OperationPreprocessors} for use with REST Assured. They can be applied to an + * {@link Operation Operation's} {@link OperationRequest request} or + * {@link OperationResponse response} before it is documented. + * + * @author Andy Wilkinson + */ +public abstract class RestAssuredPreprocessors { + + private RestAssuredPreprocessors() { + + } + + /** + * Returns a {@code UriModifyingOperationPreprocessor} that will modify URIs in the + * request or response by changing one or more of their host, scheme, and port. + * + * @return the preprocessor + */ + public static UriModifyingOperationPreprocessor modifyUris() { + return new UriModifyingOperationPreprocessor(); + } + +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java new file mode 100644 index 000000000..bb134d0cc --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -0,0 +1,249 @@ +/* + * Copyright 2012-2016 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.restassured.operation.preprocess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.preprocess.ContentModifier; +import org.springframework.restdocs.operation.preprocess.ContentModifyingOperationPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationPreprocessor; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * An {@link OperationPreprocessor} that modifies URIs in the request and in the response + * by changing one or more of their host, scheme, and port. URIs in the following + * locations are modified: + *
      + *
    • {@link OperationRequest#getUri() Request URI} + *
    • {@link OperationRequest#getHeaders() Request headers} + *
    • {@link OperationRequest#getContent() Request content} + *
    • {@link OperationRequestPart#getHeaders() Request part headers} + *
    • {@link OperationRequestPart#getContent() Request part content} + *
    • {@link OperationResponse#getHeaders() Response headers} + *
    • {@link OperationResponse#getContent() Response content} + *
    + * + * @author Andy Wilkinson + */ +public final class UriModifyingOperationPreprocessor implements OperationPreprocessor { + + private final UriModifyingContentModifier contentModifier = new UriModifyingContentModifier(); + + private final OperationPreprocessor contentModifyingDelegate = new ContentModifyingOperationPreprocessor( + this.contentModifier); + + private String scheme; + + private String host; + + private String port; + + /** + * Modifies the URI to use the given {@code scheme}. {@code null}, the default, will + * leave the scheme unchanged. + * + * @param scheme the scheme + * @return {@code this} + */ + public UriModifyingOperationPreprocessor scheme(String scheme) { + this.scheme = scheme; + this.contentModifier.setScheme(scheme); + return this; + } + + /** + * Modifies the URI to use the given {@code host}. {@code null}, the default, will + * leave the host unchanged. + * + * @param host the host + * @return {@code this} + */ + public UriModifyingOperationPreprocessor host(String host) { + this.host = host; + this.contentModifier.setHost(host); + return this; + } + + /** + * Modifies the URI to use the given {@code port}. + * + * @param port the port + * @return {@code this} + */ + public UriModifyingOperationPreprocessor port(int port) { + return port(Integer.toString(port)); + } + + /** + * Removes the port from the URI. + * + * @return {@code this} + */ + public UriModifyingOperationPreprocessor removePort() { + return port(""); + } + + private UriModifyingOperationPreprocessor port(String port) { + this.port = port; + this.contentModifier.setPort(port); + return this; + } + + @Override + public OperationRequest preprocess(OperationRequest request) { + UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUri(request.getUri()); + if (this.scheme != null) { + uriBuilder.scheme(this.scheme); + } + if (this.host != null) { + uriBuilder.host(this.host); + } + if (this.port != null) { + if (StringUtils.hasText(this.port)) { + uriBuilder.port(this.port); + } + else { + uriBuilder.port(null); + } + } + HttpHeaders modifiedHeaders = modify(request.getHeaders()); + if (this.host != null) { + modifiedHeaders.set(HttpHeaders.HOST, this.host); + } + return this.contentModifyingDelegate.preprocess(new OperationRequestFactory() + .create(uriBuilder.build().toUri(), request.getMethod(), + request.getContent(), modifiedHeaders, request.getParameters(), + modify(request.getParts()))); + } + + @Override + public OperationResponse preprocess(OperationResponse response) { + return this.contentModifyingDelegate.preprocess(new OperationResponseFactory() + .create(response.getStatus(), modify(response.getHeaders()), + response.getContent())); + } + + private HttpHeaders modify(HttpHeaders headers) { + HttpHeaders modified = new HttpHeaders(); + for (Entry> header : headers.entrySet()) { + for (String value : header.getValue()) { + modified.add(header.getKey(), this.contentModifier.modify(value)); + } + } + return modified; + } + + private Collection modify(Collection parts) { + List modifiedParts = new ArrayList<>(); + OperationRequestPartFactory factory = new OperationRequestPartFactory(); + for (OperationRequestPart part : parts) { + modifiedParts.add(factory.create(part.getName(), part.getSubmittedFileName(), + this.contentModifier.modifyContent(part.getContent(), part + .getHeaders().getContentType()), modify(part.getHeaders()))); + } + return modifiedParts; + } + + private static final class UriModifyingContentModifier implements ContentModifier { + + private static final Pattern SCHEME_HOST_PORT_PATTERN = Pattern + .compile("(http[s]?)://([^/:#?]+)(:[0-9]+)?"); + + private String scheme; + + private String host; + + private String port; + + private void setScheme(String scheme) { + this.scheme = scheme; + } + + private void setHost(String host) { + this.host = host; + } + + private void setPort(String port) { + this.port = port; + } + + @Override + public byte[] modifyContent(byte[] content, MediaType contentType) { + String input; + if (contentType != null && contentType.getCharSet() != null) { + input = new String(content, contentType.getCharSet()); + } + else { + input = new String(content); + } + + return modify(input).getBytes(); + } + + private String modify(String input) { + List replacements = Arrays.asList(this.scheme, this.host, + StringUtils.hasText(this.port) ? ":" + this.port : this.port); + + int previous = 0; + + Matcher matcher = SCHEME_HOST_PORT_PATTERN.matcher(input); + StringBuilder builder = new StringBuilder(); + while (matcher.find()) { + for (int i = 1; i <= matcher.groupCount(); i++) { + if (matcher.start(i) >= 0) { + builder.append(input.substring(previous, matcher.start(i))); + } + if (matcher.start(i) >= 0) { + previous = matcher.end(i); + } + builder.append(getReplacement(matcher.group(i), + replacements.get(i - 1))); + } + } + + if (previous < input.length()) { + builder.append(input.substring(previous)); + } + return builder.toString(); + } + + private String getReplacement(String original, String candidate) { + if (candidate != null) { + return candidate; + } + if (original != null) { + return original; + } + return ""; + } + } +} diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java new file mode 100644 index 000000000..83ffc61b7 --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2014-2016 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. + */ + +/** + * REST Assured-specific support for preprocessing an operation prior to it being + * documented. + */ +package org.springframework.restdocs.restassured.operation.preprocess; + diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java new file mode 100644 index 000000000..a8b893fae --- /dev/null +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2014-2016 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. + */ + +/** + * Core classes for using Spring REST Docs with REST Assured. + */ +package org.springframework.restdocs.restassured; + diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java new file mode 100644 index 000000000..bb5325598 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java @@ -0,0 +1,251 @@ +/* + * Copyright 2012-2016 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.restassured; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.restassured.RestAssuredOperationRequestFactoryTests.TestApplication; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link RestAssuredOperationRequestFactory}. + * + * @author Andy Wilkinson + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = TestApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port=0") +public class RestAssuredOperationRequestFactoryTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final RestAssuredOperationRequestFactory factory = new RestAssuredOperationRequestFactory(); + + @Value("${local.server.port}") + private int port; + + @Test + public void requestUri() { + RequestSpecification requestSpec = RestAssured.given().port(this.port); + requestSpec.get("/foo/bar"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getUri(), + is(equalTo(URI.create("http://localhost:" + this.port + "/foo/bar")))); + } + + @Test + public void requestMethod() { + RequestSpecification requestSpec = RestAssured.given().port(this.port); + requestSpec.head("/foo/bar"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getMethod(), is(equalTo(HttpMethod.HEAD))); + } + + @Test + public void queryStringParameters() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .queryParam("foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters().size(), is(1)); + assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + } + + @Test + public void queryStringFromUrlParameters() { + RequestSpecification requestSpec = RestAssured.given().port(this.port); + requestSpec.get("/?foo=bar"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters().size(), is(1)); + assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + } + + @Test + public void formParameters() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .formParameter("foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters().size(), is(1)); + assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + } + + @Test + public void requestParameters() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .parameter("foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getParameters().size(), is(1)); + assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); + } + + @Test + public void headers() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .header("Foo", "bar"); + requestSpec.get("/"); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getHeaders().toString(), request.getHeaders().size(), is(3)); + assertThat(request.getHeaders().get("Foo"), is(equalTo(Arrays.asList("bar")))); + assertThat(request.getHeaders().get("Accept"), is(equalTo(Arrays.asList("*/*")))); + assertThat(request.getHeaders().get("Host"), + is(equalTo(Arrays.asList("localhost")))); + } + + @Test + public void multipart() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("a", "a.txt", "alpha", null) + .multiPart("b", new ObjectBody("bar"), "application/json"); + requestSpec.post().then().statusCode(200); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + Collection parts = request.getParts(); + assertThat(parts.size(), is(2)); + Iterator iterator = parts.iterator(); + OperationRequestPart part = iterator.next(); + assertThat(part.getName(), is(equalTo("a"))); + assertThat(part.getSubmittedFileName(), is(equalTo("a.txt"))); + assertThat(part.getContentAsString(), is(equalTo("alpha"))); + assertThat(part.getHeaders().getContentType(), is(equalTo(MediaType.TEXT_PLAIN))); + part = iterator.next(); + assertThat(part.getName(), is(equalTo("b"))); + assertThat(part.getSubmittedFileName(), is(equalTo("file"))); + assertThat(part.getContentAsString(), is(equalTo("{\"foo\":\"bar\"}"))); + assertThat(part.getHeaders().getContentType(), + is(equalTo(MediaType.APPLICATION_JSON))); + } + + @Test + public void byteArrayBody() { + RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()) + .port(this.port); + requestSpec.post(); + this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + } + + @Test + public void stringBody() { + RequestSpecification requestSpec = RestAssured.given().body("body") + .port(this.port); + requestSpec.post(); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString(), is(equalTo("body"))); + } + + @Test + public void objectBody() { + RequestSpecification requestSpec = RestAssured.given() + .body(new ObjectBody("bar")).port(this.port); + requestSpec.post(); + OperationRequest request = this.factory + .createOperationRequest((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString(), is(equalTo("{\"foo\":\"bar\"}"))); + } + + @Test + public void inputStreamBodyIsNotSupported() { + RequestSpecification requestSpec = RestAssured.given() + .body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) + .port(this.port); + requestSpec.post(); + this.thrown + .expectMessage(equalTo("Unsupported request content: java.io.ByteArrayInputStream")); + this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + } + + @Test + public void fileBodyIsNotSupported() { + RequestSpecification requestSpec = RestAssured.given() + .body(new File("src/test/resources/body.txt")).port(this.port); + requestSpec.post(); + this.thrown.expectMessage(equalTo("Unsupported request content: java.io.File")); + this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + } + + /** + * Minimal test web application. + */ + @Configuration + @EnableAutoConfiguration + @RestController + static class TestApplication { + + @RequestMapping("/") + void handle() { + } + + } + + /** + * Sample object body to verify JSON serialization. + */ + static class ObjectBody { + + private final String foo; + + ObjectBody(String foo) { + this.foo = foo; + } + + public String getFoo() { + return this.foo; + } + + } + +} diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java new file mode 100644 index 000000000..1f7bbbd44 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2016 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.restassured; + +import java.util.List; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.templates.TemplateEngine; + +import com.jayway.restassured.filter.FilterContext; +import com.jayway.restassured.specification.FilterableRequestSpecification; +import com.jayway.restassured.specification.FilterableResponseSpecification; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link RestAssuredRestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +public class RestAssuredRestDocumentationConfigurerTests { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + private final FilterableRequestSpecification requestSpec = mock(FilterableRequestSpecification.class); + + private final FilterableResponseSpecification responseSpec = mock(FilterableResponseSpecification.class); + + private final FilterContext filterContext = mock(FilterContext.class); + + private final RestAssuredRestDocumentationConfigurer configurer = new RestAssuredRestDocumentationConfigurer( + this.restDocumentation); + + @Test + public void nextFilterIsCalled() { + this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); + verify(this.filterContext).setValue( + eq("org.springframework.restdocs.configuration"), any(Map.class)); + } + + @Test + public void configurationIsAddedToTheContext() { + this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); + @SuppressWarnings("rawtypes") + ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(Map.class); + verify(this.filterContext).setValue( + eq("org.springframework.restdocs.configuration"), + configurationCaptor.capture()); + @SuppressWarnings("unchecked") + Map configuration = configurationCaptor.getValue(); + assertThat( + configuration, + hasEntry(equalTo(TemplateEngine.class.getName()), + instanceOf(TemplateEngine.class))); + assertThat( + configuration, + hasEntry(equalTo(WriterResolver.class.getName()), + instanceOf(WriterResolver.class))); + assertThat( + configuration, + hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + instanceOf(List.class))); + } +} diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java new file mode 100644 index 000000000..3ae6d13f5 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -0,0 +1,374 @@ +/* + * Copyright 2014-2016 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.restassured; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.hypermedia.Link; +import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static com.jayway.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.maskLinks; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.removeHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.replacePattern; +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.requestParameters; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; +import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; +import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; +import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; +import static org.springframework.restdocs.test.SnippetMatchers.snippet; + +/** + * Integration tests for using Spring REST Docs with REST Assured. + * + * @author Andy Wilkinson + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = TestApplication.class) +@WebAppConfiguration +@IntegrationTest("server.port=0") +public class RestAssuredRestDocumentationIntegrationTests { + + @Rule + public RestDocumentation restDocumentation = new RestDocumentation( + "build/generated-snippets"); + + @Value("${local.server.port}") + private int port; + + @Test + public void defaultSnippetGeneration() { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("default")).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/default"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void curlSnippetWithContent() throws Exception { + String contentType = "text/plain; charset=UTF-8"; + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-content")).accept("application/json") + .content("content").contentType(contentType).post("/").then() + .statusCode(200); + + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl 'http://localhost:" + this.port + "/' -i " + + "-X POST -H 'Accept: application/json' " + + "-H 'Content-Type: " + contentType + "' " + + "-d 'content'")))); + } + + @Test + public void curlSnippetWithQueryStringOnPost() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-query-string")) + .accept("application/json").param("foo", "bar").param("a", "alpha") + .post("/?foo=bar").then().statusCode(200); + String contentType = "application/x-www-form-urlencoded; charset=ISO-8859-1"; + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl " + "'http://localhost:" + this.port + + "/?foo=bar' -i -X POST " + + "-H 'Accept: application/json' " + + "-H 'Content-Type: " + contentType + "' " + + "-d 'a=alpha'")))); + } + + @Test + public void linksSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("links", + links(linkWithRel("rel").description("The description")))) + .accept("application/json").get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "links.adoc"); + } + + @Test + public void pathParametersSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document( + "path-parameters", + pathParameters(parameterWithName("foo").description( + "The description")))).accept("application/json") + .get("/{foo}", "").then().statusCode(200); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/path-parameters"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "path-parameters.adoc"); + } + + @Test + public void requestParametersSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document( + "request-parameters", + requestParameters(parameterWithName("foo").description( + "The description")))).accept("application/json") + .param("foo", "bar").get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/request-parameters"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-parameters.adoc"); + } + + @Test + public void requestFieldsSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("request-fields", requestFields(fieldWithPath("a") + .description("The description")))).accept("application/json") + .content("{\"a\":\"alpha\"}").post("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/request-fields"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-fields.adoc"); + } + + @Test + public void responseFieldsSnippet() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document( + "response-fields", + responseFields( + fieldWithPath("a").description("The description"), + fieldWithPath("links").description( + "Links to other resources")))) + .accept("application/json").get("/").then().statusCode(200); + + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/response-fields"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "response-fields.adoc"); + } + + @Test + public void parameterizedOutputDirectory() throws Exception { + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("{method-name}")).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/parameterized-output-directory"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void multiStep() throws Exception { + RequestSpecification spec = new RequestSpecBuilder().setPort(this.port) + .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(document("{method-name}-{step}")).build(); + given(spec).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + given(spec).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + given(spec).get("/").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc"); + } + + @Test + public void preprocessedRequest() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .header("a", "alpha") + .header("b", "bravo") + .contentType("application/json") + .accept("application/json") + .content("{\"a\":\"alpha\"}") + .filter(document("original-request")) + .filter(document( + "preprocessed-request", + preprocessRequest( + prettyPrint(), + removeHeaders("a", HttpHeaders.HOST, + HttpHeaders.CONTENT_LENGTH), + replacePattern(pattern, "\"<>\"")))).get("/") + .then().statusCode(200); + assertThat( + new File("build/generated-snippets/original-request/http-request.adoc"), + is(snippet() + .withContents( + httpRequest(RequestMethod.GET, "/") + .header("a", "alpha") + .header("b", "bravo") + .header("Accept", + MediaType.APPLICATION_JSON_VALUE) + .header("Content-Type", + "application/json; charset=UTF-8") + .header("Host", "localhost") + .header("Content-Length", "13") + .content("{\"a\":\"alpha\"}")))); + String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); + assertThat( + new File( + "build/generated-snippets/preprocessed-request/http-request.adoc"), + is(snippet() + .withContents( + httpRequest(RequestMethod.GET, "/") + .header("b", "bravo") + .header("Accept", + MediaType.APPLICATION_JSON_VALUE) + .header("Content-Type", + "application/json; charset=UTF-8") + .content(prettyPrinted)))); + } + + @Test + public void preprocessedResponse() throws Exception { + Pattern pattern = Pattern.compile("(\"alpha\")"); + given().port(this.port) + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("original-response")) + .filter(document( + "preprocessed-response", + preprocessResponse( + prettyPrint(), + maskLinks(), + removeHeaders("a", "Transfer-Encoding", "Date", "Server"), + replacePattern(pattern, "\"<>\""), modifyUris() + .scheme("https").host("api.example.com") + .removePort()))).get("/").then().statusCode(200); + String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); + assertThat( + new File( + "build/generated-snippets/preprocessed-response/http-response.adoc"), + is(snippet().withContents( + httpResponse(HttpStatus.OK) + .header("Foo", "https://api.example.com/foo/bar") + .header("Content-Type", "application/json;charset=UTF-8") + .header(HttpHeaders.CONTENT_LENGTH, + prettyPrinted.getBytes().length) + .content(prettyPrinted)))); + } + + @Test + public void customSnippetTemplate() throws Exception { + ClassLoader classLoader = new URLClassLoader(new URL[] { new File( + "src/test/resources/custom-snippet-templates").toURI().toURL() }, + getClass().getClassLoader()); + ClassLoader previous = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoader); + try { + given().port(this.port).accept("application/json") + .filter(documentationConfiguration(this.restDocumentation)) + .filter(document("custom-snippet-template")).get("/").then() + .statusCode(200); + } + finally { + Thread.currentThread().setContextClassLoader(previous); + } + assertThat(new File( + "build/generated-snippets/custom-snippet-template/curl-request.adoc"), + is(snippet().withContents(equalTo("Custom curl request")))); + } + + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { + for (String snippet : snippets) { + File snippetFile = new File(directory, snippet); + assertTrue("Snippet " + snippetFile + " not found", snippetFile.isFile()); + } + } + + /** + * Minimal test application called by the tests. + */ + @Configuration + @EnableAutoConfiguration + @RestController + static class TestApplication { + + @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> foo() { + Map response = new HashMap<>(); + response.put("a", "alpha"); + response.put("links", Arrays.asList(new Link("rel", "href"))); + HttpHeaders headers = new HttpHeaders(); + headers.add("a", "alpha"); + headers.add("Foo", "http://localhost:12345/foo/bar"); + return new ResponseEntity<>(response, headers, HttpStatus.OK); + } + + @RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE) + public String bar() { + return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; + } + + } + +} diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java new file mode 100644 index 000000000..a962c782a --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -0,0 +1,343 @@ +/* + * Copyright 2012-2016 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.restassured.operation.preprocess; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.OperationRequestPartFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.Parameters; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link UriModifyingOperationPreprocessor}. + * + * @author Andy Wilkinson + */ +public class UriModifyingOperationPreprocessorTests { + + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + + private final UriModifyingOperationPreprocessor preprocessor = new UriModifyingOperationPreprocessor(); + + @Test + public void requestUriSchemeCanBeModified() { + this.preprocessor.scheme("https"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://localhost:12345")); + assertThat(processed.getUri(), is(equalTo(URI.create("https://localhost:12345")))); + } + + @Test + public void requestUriHostCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com:12345")))); + } + + @Test + public void requestUriPortCanBeModified() { + this.preprocessor.port(23456); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com:23456")))); + } + + @Test + public void requestUriPortCanBeRemoved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345")); + assertThat(processed.getUri(), is(equalTo(URI.create("http://api.example.com")))); + } + + @Test + public void requestUriPathIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345/foo/bar")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com/foo/bar")))); + } + + @Test + public void requestUriQueryIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345?foo=bar")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com?foo=bar")))); + } + + @Test + public void requestUriAnchorIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://api.example.com:12345#foo")); + assertThat(processed.getUri(), + is(equalTo(URI.create("http://api.example.com#foo")))); + } + + @Test + public void requestContentUriSchemeCanBeModified() { + this.preprocessor.scheme("https"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'https://localhost:12345' should be used"))); + } + + @Test + public void requestContentUriHostCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://api.example.com:12345' should be used"))); + } + + @Test + public void requestContentUriPortCanBeModified() { + this.preprocessor.port(23456); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost:23456' should be used"))); + } + + @Test + public void requestContentUriPortCanBeRemoved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost' should be used"))); + } + + @Test + public void multipleRequestContentUrisCanBeModified() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); + assertThat( + new String(processed.getContent()), + is(equalTo("Use 'http://localhost' or 'https://localhost' to access the service"))); + } + + @Test + public void requestContentUriPathIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost/foo/bar' should be used"))); + } + + @Test + public void requestContentUriQueryIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345?foo=bar' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost?foo=bar' should be used"))); + } + + @Test + public void requestContentUriAnchorIsPreserved() { + this.preprocessor.removePort(); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithContent("The uri 'http://localhost:12345#foo' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost#foo' should be used"))); + } + + @Test + public void responseContentUriSchemeCanBeModified() { + this.preprocessor.scheme("https"); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'https://localhost:12345' should be used"))); + } + + @Test + public void responseContentUriHostCanBeModified() { + this.preprocessor.host("api.example.com"); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://api.example.com:12345' should be used"))); + } + + @Test + public void responseContentUriPortCanBeModified() { + this.preprocessor.port(23456); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost:23456' should be used"))); + } + + @Test + public void responseContentUriPortCanBeRemoved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost' should be used"))); + } + + @Test + public void multipleResponseContentUrisCanBeModified() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("Use 'http://localhost:12345' or 'https://localhost:23456' to access the service")); + assertThat( + new String(processed.getContent()), + is(equalTo("Use 'http://localhost' or 'https://localhost' to access the service"))); + } + + @Test + public void responseContentUriPathIsPreserved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345/foo/bar' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost/foo/bar' should be used"))); + } + + @Test + public void responseContentUriQueryIsPreserved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345?foo=bar' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost?foo=bar' should be used"))); + } + + @Test + public void responseContentUriAnchorIsPreserved() { + this.preprocessor.removePort(); + OperationResponse processed = this.preprocessor + .preprocess(createResponseWithContent("The uri 'http://localhost:12345#foo' should be used")); + assertThat(new String(processed.getContent()), + is(equalTo("The uri 'http://localhost#foo' should be used"))); + } + + @Test + public void urisInRequestHeadersCanBeModified() { + OperationRequest processed = this.preprocessor.host("api.example.com") + .preprocess(createRequestWithHeader("Foo", "http://locahost:12345")); + assertThat(processed.getHeaders().getFirst("Foo"), + is(equalTo("http://api.example.com:12345"))); + assertThat(processed.getHeaders().getFirst("Host"), + is(equalTo("api.example.com"))); + } + + @Test + public void urisInResponseHeadersCanBeModified() { + OperationResponse processed = this.preprocessor.host("api.example.com") + .preprocess(createResponseWithHeader("Foo", "http://locahost:12345")); + assertThat(processed.getHeaders().getFirst("Foo"), + is(equalTo("http://api.example.com:12345"))); + } + + @Test + public void urisInRequestPartHeadersCanBeModified() { + OperationRequest processed = this.preprocessor.host("api.example.com") + .preprocess( + createRequestWithPartWithHeader("Foo", "http://locahost:12345")); + assertThat(processed.getParts().iterator().next().getHeaders().getFirst("Foo"), + is(equalTo("http://api.example.com:12345"))); + } + + @Test + public void urisInRequestPartContentCanBeModified() { + OperationRequest processed = this.preprocessor + .host("api.example.com") + .preprocess( + createRequestWithPartWithContent("The uri 'http://localhost:12345' should be used")); + assertThat(new String(processed.getParts().iterator().next().getContent()), + is(equalTo("The uri 'http://api.example.com:12345' should be used"))); + } + + private OperationRequest createRequestWithUri(String uri) { + return this.requestFactory.create(URI.create(uri), HttpMethod.GET, new byte[0], + new HttpHeaders(), new Parameters(), + Collections.emptyList()); + } + + private OperationRequest createRequestWithContent(String content) { + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + content.getBytes(), new HttpHeaders(), new Parameters(), + Collections.emptyList()); + } + + private OperationRequest createRequestWithHeader(String name, String value) { + HttpHeaders headers = new HttpHeaders(); + headers.add(name, value); + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + new byte[0], headers, new Parameters(), + Collections.emptyList()); + } + + private OperationRequest createRequestWithPartWithHeader(String name, String value) { + HttpHeaders headers = new HttpHeaders(); + headers.add(name, value); + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + new byte[0], new HttpHeaders(), new Parameters(), Arrays + .asList(new OperationRequestPartFactory().create("part", + "fileName", new byte[0], headers))); + } + + private OperationRequest createRequestWithPartWithContent(String content) { + return this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, + new byte[0], new HttpHeaders(), new Parameters(), Arrays + .asList(new OperationRequestPartFactory().create("part", + "fileName", content.getBytes(), new HttpHeaders()))); + } + + private OperationResponse createResponseWithContent(String content) { + return this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), + content.getBytes()); + } + + private OperationResponse createResponseWithHeader(String name, String value) { + HttpHeaders headers = new HttpHeaders(); + headers.add(name, value); + return this.responseFactory.create(HttpStatus.OK, headers, new byte[0]); + } + +} diff --git a/spring-restdocs-restassured/src/test/resources/body.txt b/spring-restdocs-restassured/src/test/resources/body.txt new file mode 100644 index 000000000..1a010b1c0 --- /dev/null +++ b/spring-restdocs-restassured/src/test/resources/body.txt @@ -0,0 +1 @@ +file \ No newline at end of file diff --git a/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet b/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet new file mode 100644 index 000000000..07f3a48ff --- /dev/null +++ b/spring-restdocs-restassured/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet @@ -0,0 +1 @@ +Custom curl request \ No newline at end of file From 5dbb31f38a1d0bf3f1e93c2c57827c72d79ede6f Mon Sep 17 00:00:00 2001 From: mnhock Date: Fri, 22 Jan 2016 16:55:03 +0100 Subject: [PATCH 046/898] Fix String comparison in HttpRequestSnippet Closes gh-186 --- .../org/springframework/restdocs/http/HttpRequestSnippet.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 908b2fb84..26d209fab 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -82,7 +82,7 @@ private List> getHeaders(OperationRequest request) { for (Entry> header : request.getHeaders().entrySet()) { for (String value : header.getValue()) { - if (header.getKey() == HttpHeaders.CONTENT_TYPE + if (HttpHeaders.CONTENT_TYPE.equals(header.getKey()) && !request.getParts().isEmpty()) { headers.add(header(header.getKey(), String.format("%s; boundary=%s", value, MULTIPART_BOUNDARY))); From 91d6e2f8ec7c52671cf76941e4881f1aecdaad49 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 1 Feb 2016 12:37:23 +0000 Subject: [PATCH 047/898] Improve extensibility and reusability of MustacheTemplateEngine Previously, MustacheTemplateEngine hard coded the configuration of its Mustache Compiler. This commit adds an overloaded constructor that allows a custom Compiler to be used. It also adds protected accessors for the compiler and the template resource resolver so that they can be easily accessed by subclasses. Closes gh-149 --- .../mustache/MustacheTemplateEngine.java | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index df142045a..9e372ac4c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -36,18 +36,32 @@ */ public class MustacheTemplateEngine implements TemplateEngine { - private final Compiler compiler = Mustache.compiler().escapeHTML(false); + private final Compiler compiler; private final TemplateResourceResolver templateResourceResolver; /** - * Creates a new {@link MustacheTemplateEngine} that will use the given + * Creates a new {@code MustacheTemplateEngine} that will use the given * {@code templateResourceResolver} to resolve template paths. * - * @param templateResourceResolver The resolve to use + * @param templateResourceResolver the resolver to use */ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) { + this(templateResourceResolver, Mustache.compiler().escapeHTML(false)); + } + + /** + * Creates a new {@code MustacheTemplateEngine} that will use the given + * {@code tempalteResourceResolver} to resolve templates and the given + * {@code compiler} to compile them. + * + * @param templateResourceResolver the resolver to use + * @param compiler the compiler to use + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, + Compiler compiler) { this.templateResourceResolver = templateResourceResolver; + this.compiler = compiler; } @Override @@ -58,4 +72,23 @@ public Template compileTemplate(String name) throws IOException { templateResource.getInputStream()))); } + /** + * Returns the {@link Compiler} used to compile Mustache templates. + * + * @return the compiler + */ + protected final Compiler getCompiler() { + return this.compiler; + } + + /** + * Returns the {@link TemplateResourceResolver} used to resolve the template resources + * prior to compilation. + * + * @return the resolver + */ + protected final TemplateResourceResolver getTemplateResourceResolver() { + return this.templateResourceResolver; + } + } From 29675728aa20cf36e6674a89708c502a3e324d07 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 2 Feb 2016 09:32:17 +0000 Subject: [PATCH 048/898] Introduce a code of conduct --- CODE_OF_CONDUCT.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 15 +++++++++++---- README.md | 12 +++++++----- 3 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..4df5b8d41 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open +and welcoming community, we pledge to respect all people who contribute through reporting +issues, posting feature requests, updating documentation, submitting pull requests or +patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, body size, race, ethnicity, age, +religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, + without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, +commits, code, wiki edits, issues, and other contributions that are not aligned to this +Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors +that they deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to fairly and +consistently applying these principles to every aspect of managing this project. Project +maintainers who do not follow or enforce the Code of Conduct may be permanently removed +from the project team. + +This Code of Conduct applies both within project spaces and in public spaces when an +individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by +contacting a project maintainer at spring-code-of-conduct@pivotal.io. All complaints will +be reviewed and investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. Maintainers are obligated to maintain confidentiality +with regard to the reporter of an incident. + +This Code of Conduct is adapted from the [Contributor Covenant][1], version 1.3.0, available +at [contributor-covenant.org/version/1/3/0/][2]. + +[1]: http://contributor-covenant.org +[2]: http://contributor-covenant.org/version/1/3/0/ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d58b56368..5b609d53c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,10 +4,16 @@ Spring REST Docs is released under the Apache 2.0 license. If you would like to contribute something, or simply want to work with the code, this document should help you to get started. +## Code of conduct + +This project adheres to the Contributor Covenant [code of conduct][1]. By participating, +you are expected to uphold this code. Please report unacceptable behavior to +spring-code-of-conduct@pivotal.io. + ## Sign the contributor license agreement Before we accept a non-trivial patch or pull request we will need you to sign the -[contributor's license agreement (CLA)][1]. Signing the contributor's agreement does not +[contributor's license agreement (CLA)][2]. Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. Please use "Andy Wilkinson" in the project lead field when you complete the form. @@ -28,7 +34,7 @@ None of these is essential for a pull request, but they will all help - Add unit tests that covers and new or modified functionality - Whenever possible, please rebase your branch against the current master (or other target branch in the main project). -* When writing a commit message please follow [these conventions][2]. Also, if you are +* When writing a commit message please follow [these conventions][3]. Also, if you are fixing an existing issue please add `Fixes gh-nnn` at the end of the commit message (where nnn is the issue number). @@ -54,5 +60,6 @@ $ ./gradlew eclipse The project can then be imported into Eclipse using `File -> Import…` and then selecting `General -> Existing Projects into Workspace`. -[1]: https://support.springsource.com/spring_committer_signup -[2]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html \ No newline at end of file +[1]: CODE_OF_CONDUCT.md +[2]: https://support.springsource.com/spring_committer_signup +[3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html \ No newline at end of file diff --git a/README.md b/README.md index 8ca99f410..aaa0c6dcc 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,12 @@ Spring REST Docs requires Java 7 or later and is built using [Gradle][10]: ## Contributing -[Pull requests][11] are welcome. Please see the [contributor guidelines][12] for details. +Contributors to this project agree to uphold its [code of conduct][11]. +[Pull requests][12] are welcome. Please see the [contributor guidelines][13] for details. ## Licence -Spring REST Docs is open source software released under the [Apache 2.0 license][13]. +Spring REST Docs is open source software released under the [Apache 2.0 license][14]. [1]: https://build.spring.io/plugins/servlet/buildStatusImage/SRD-PUB (Build status) [2]: https://build.spring.io/browse/SRD-PUB @@ -41,7 +42,8 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [8]: https://www.youtube.com/watch?v=knH5ihPNiUs&feature=youtu.be [9]: http://docs.spring.io/spring-restdocs/docs/ [10]: http://gradle.org -[11]: https://help.github.com/articles/using-pull-requests/ -[12]: https://github.com/spring-projects/spring-restdocs/blob/master/CONTRIBUTING.md -[13]: http://www.apache.org/licenses/LICENSE-2.0.html +[11]: CODE_OF_CONDUCT.md +[12]: https://help.github.com/articles/using-pull-requests/ +[13]: CONTRIBUTING.md +[14]: http://www.apache.org/licenses/LICENSE-2.0.html From 4d37dea1d6d8ef21acea5043b4280799e7110207 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Jan 2016 16:10:00 +0000 Subject: [PATCH 049/898] Add support for generating snippets in Markdown This commit introduces support for generating snippets formatted using Markdown. Asciidoctor remains the default. A new SnippetFormat abstraction has been introduced with Asciidoctor and Markdown implementations provided out of the box. Markdown-formatted templates are also provided for all of the default snippets. Please refer to the updated reference documentation for further details. Closes gh-150 Closes gh-19 --- config/checkstyle/checkstyle.xml | 2 +- docs/src/docs/asciidoc/configuration.adoc | 23 +- docs/src/docs/asciidoc/index.adoc | 1 + docs/src/docs/asciidoc/introduction.adoc | 5 +- .../docs/asciidoc/working-with-markdown.adoc | 28 +++ .../com/example/mockmvc/CustomFormat.java | 50 ++++ .../com/example/restassured/CustomFormat.java | 46 ++++ .../config/RestDocumentationConfigurer.java | 19 +- .../restdocs/config/SnippetConfiguration.java | 45 ++++ .../restdocs/config/SnippetConfigurer.java | 30 ++- .../restdocs/snippet/SnippetFormat.java | 34 +++ .../restdocs/snippet/SnippetFormats.java | 74 ++++++ .../snippet/StandardWriterResolver.java | 36 ++- .../restdocs/snippet/WriterResolver.java | 5 +- .../StandardTemplateResourceResolver.java | 39 +++- .../curl-request.snippet} | 0 .../http-request.snippet} | 0 .../http-response.snippet} | 0 .../links.snippet} | 0 .../path-parameters.snippet} | 0 .../request-fields.snippet} | 0 .../request-headers.snippet} | 0 .../request-parameters.snippet} | 0 .../response-fields.snippet} | 0 .../response-headers.snippet} | 0 .../templates/md/curl-request.snippet | 3 + .../templates/md/http-request.snippet | 7 + .../templates/md/http-response.snippet | 7 + .../restdocs/templates/md/links.snippet | 5 + .../templates/md/path-parameters.snippet | 6 + .../templates/md/request-fields.snippet | 5 + .../templates/md/request-headers.snippet | 5 + .../templates/md/request-parameters.snippet | 5 + .../templates/md/response-fields.snippet | 5 + .../templates/md/response-headers.snippet | 5 + .../restdocs/AbstractSnippetTests.java | 96 ++++++++ .../curl/CurlRequestSnippetTests.java | 145 +++++------- .../RequestHeadersSnippetFailureTests.java | 72 ++++++ .../headers/RequestHeadersSnippetTests.java | 113 +++------ .../ResponseHeadersSnippetFailureTests.java | 73 ++++++ .../headers/ResponseHeadersSnippetTests.java | 114 +++------ .../http/HttpRequestSnippetTests.java | 64 ++---- .../http/HttpResponseSnippetTests.java | 39 ++-- .../hypermedia/LinksSnippetFailureTests.java | 79 +++++++ .../hypermedia/LinksSnippetTests.java | 141 +++--------- .../hypermedia/StubLinkExtractor.java | 47 ++++ .../AsciidoctorRequestFieldsSnippetTests.java | 74 ++++++ .../RequestFieldsSnippetFailureTests.java | 162 +++++++++++++ .../payload/RequestFieldsSnippetTests.java | 217 +++--------------- .../ResponseFieldsSnippetFailureTests.java | 143 ++++++++++++ .../payload/ResponseFieldsSnippetTests.java | 207 ++++------------- .../PathParametersSnippetFailureTests.java | 85 +++++++ .../request/PathParametersSnippetTests.java | 127 +++------- .../RequestParametersSnippetFailureTests.java | 84 +++++++ .../RequestParametersSnippetTests.java | 104 ++------- .../snippet/StandardWriterResolverTests.java | 5 +- ...StandardTemplateResourceResolverTests.java | 8 +- .../restdocs/test/ExpectedSnippet.java | 15 +- .../restdocs/test/OperationBuilder.java | 17 +- .../restdocs/test/SnippetMatchers.java | 212 ++++++++++++++--- .../curl-request-with-title.snippet | 0 .../http-request-with-title.snippet | 0 .../http-response-with-title.snippet | 0 .../links-with-extra-column.snippet | 0 .../{ => adoc}/links-with-title.snippet | 0 .../path-parameters-with-extra-column.snippet | 0 .../path-parameters-with-title.snippet | 0 .../request-fields-with-extra-column.snippet | 0 ...quest-fields-with-list-description.snippet | 0 .../request-fields-with-title.snippet | 0 .../request-headers-with-extra-column.snippet | 0 .../request-headers-with-title.snippet | 0 ...quest-parameters-with-extra-column.snippet | 0 .../request-parameters-with-title.snippet | 0 .../response-fields-with-extra-column.snippet | 0 .../response-fields-with-title.snippet | 0 ...response-headers-with-extra-column.snippet | 0 .../response-headers-with-title.snippet | 0 .../md/curl-request-with-title.snippet | 4 + .../md/http-request-with-title.snippet | 8 + .../md/http-response-with-title.snippet | 8 + .../md/links-with-extra-column.snippet | 5 + .../md/links-with-title.snippet | 6 + .../path-parameters-with-extra-column.snippet | 5 + .../md/path-parameters-with-title.snippet | 6 + .../request-fields-with-extra-column.snippet | 5 + .../md/request-fields-with-title.snippet | 6 + .../request-headers-with-extra-column.snippet | 5 + .../md/request-headers-with-title.snippet | 6 + ...quest-parameters-with-extra-column.snippet | 5 + .../md/request-parameters-with-title.snippet | 6 + .../response-fields-with-extra-column.snippet | 5 + .../md/response-fields-with-title.snippet | 6 + ...response-headers-with-extra-column.snippet | 5 + .../md/response-headers-with-title.snippet | 6 + .../MockMvcRestDocumentationConfigurer.java | 4 +- ...kMvcRestDocumentationIntegrationTests.java | 54 +++-- ...estAssuredRestDocumentationConfigurer.java | 4 +- ...uredRestDocumentationIntegrationTests.java | 23 +- 99 files changed, 2089 insertions(+), 1026 deletions(-) create mode 100644 docs/src/docs/asciidoc/working-with-markdown.adoc create mode 100644 docs/src/test/java/com/example/mockmvc/CustomFormat.java create mode 100644 docs/src/test/java/com/example/restassured/CustomFormat.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-curl-request.snippet => adoc/curl-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-http-request.snippet => adoc/http-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-http-response.snippet => adoc/http-response.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-links.snippet => adoc/links.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-path-parameters.snippet => adoc/path-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-request-fields.snippet => adoc/request-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-request-headers.snippet => adoc/request-headers.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-request-parameters.snippet => adoc/request-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-response-fields.snippet => adoc/response-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{default-response-headers.snippet => adoc/response-headers.snippet} (100%) create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/curl-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/http-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/http-response-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/links-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/links-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/path-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/path-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-fields-with-list-description.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-headers-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/request-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/response-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/response-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/response-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{ => adoc}/response-headers-with-title.snippet (100%) create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index ebc91d6ff..4c188cd45 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.snippet.SnippetFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 239588003..f068507e9 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -42,8 +42,7 @@ TIP: To configure a request's context path, use the `contextPath` method on [[configuration-snippet-encoding]] === Snippet encoding -The default encoding used by Asciidoctor is `UTF-8`. Spring REST Docs adopts the same -default for the snippets that it generates. You can change the default snippet encoding +The default snippet encoding is `UTF-8`. You can change the default snippet encoding using the `RestDocumentationConfigurer` API. For example, to use `ISO-8859-1`: [source,java,indent=0,role="primary"] @@ -60,6 +59,26 @@ include::{examples-dir}/com/example/restassured/CustomEncoding.java[tags=custom- +[[configuration-snippet-format]] +=== Snippet format + +The default snippet format is Asciidoctor. Markdown is also supported out of the box. You +can change the default format using the `RestDocumentationConfigurer` API: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/CustomFormat.java[tags=custom-format] +---- + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/CustomFormat.java[tags=custom-format] +---- + + + [[configuration-default-snippets]] === Default snippets diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 17cde86ea..10777c6af 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -26,4 +26,5 @@ include::documenting-your-api.adoc[] include::customizing-requests-and-responses.adoc[] include::configuration.adoc[] include::working-with-asciidoctor.adoc[] +include::working-with-markdown.adoc[] include::contributing.adoc[] \ No newline at end of file diff --git a/docs/src/docs/asciidoc/introduction.adoc b/docs/src/docs/asciidoc/introduction.adoc index 3bfaef7d3..7a6fde223 100644 --- a/docs/src/docs/asciidoc/introduction.adoc +++ b/docs/src/docs/asciidoc/introduction.adoc @@ -6,8 +6,9 @@ services that is accurate and readable. Writing high-quality documentation is difficult. One way to ease that difficulty is to use tools that are well-suited to the job. To this end, Spring REST Docs uses -http://asciidoctor.org[Asciidoctor]. Asciidoctor processes plain text and produces -HTML, styled and layed out to suit your needs. +http://asciidoctor.org[Asciidoctor] by default. Asciidoctor processes plain text and +produces HTML, styled and layed out to suit your needs. If you prefer, Spring REST Docs +can also be configured to use Markdown. Spring REST Docs makes use of snippets produced by tests written with {spring-framework-docs}/#spring-mvc-test-framework[Spring MVC Test] or diff --git a/docs/src/docs/asciidoc/working-with-markdown.adoc b/docs/src/docs/asciidoc/working-with-markdown.adoc new file mode 100644 index 000000000..6f6d8ed78 --- /dev/null +++ b/docs/src/docs/asciidoc/working-with-markdown.adoc @@ -0,0 +1,28 @@ +[[working-with-markdown]] +== Working with Markdown + +This section describes any aspects of working with Markdown that are particularly +relevant to Spring REST Docs. + + + +[[working-with-markdown-limitations]] +=== Limitations + +Markdown was originally designed for people writing for the web and, as such, isn't +as well-suited to writing documentation as Asciidoctor. Typically, these limitations +are overcome by using another tool that builds on top of Markdown. + +Markdown has no official support for tables. Spring REST Docs' default Markdown snippet +templates use https://michelf.ca/projects/php-markdown/extra/#table[Markdown Extra's table +format]. + + + +[[working-with-markdown-including-snippets]] +=== Including snippets + +Markdown has no built-in support for including one Markdown file in another. To include +the generated snippets of Markdown in your documentation, you should use an additional +tool that supports this functionality. One example that's particularly well-suited to +documenting APIs is https://github.com/tripit/slate[Slate]. \ No newline at end of file diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java new file mode 100644 index 000000000..ab121224a --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2016 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 com.example.mockmvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +public class CustomFormat { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @Before + public void setUp() { + // tag::custom-format[] + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation) + .snippets().withFormat(SnippetFormats.markdown())) + .build(); + // end::custom-format[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java new file mode 100644 index 000000000..649becb25 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.junit.Before; +import org.junit.Rule; +import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.snippet.SnippetFormats; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class CustomFormat { + + @Rule + public final RestDocumentation restDocumentation = new RestDocumentation("build"); + + private RequestSpecification spec; + + @Before + public void setUp() { + // tag::custom-format[] + this.spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(this.restDocumentation) + .snippets().withFormat(SnippetFormats.markdown())) + .build(); + // end::custom-format[] + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index bedec85ae..0f0ff9b21 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -93,13 +93,20 @@ protected final AbstractConfigurer getTemplateEngineConfigurer() { private static final class TemplateEngineConfigurer extends AbstractConfigurer { - private TemplateEngine templateEngine = new MustacheTemplateEngine( - new StandardTemplateResourceResolver()); + private TemplateEngine templateEngine; @Override public void apply(Map configuration, RestDocumentationContext context) { - configuration.put(TemplateEngine.class.getName(), this.templateEngine); + TemplateEngine engineToUse = this.templateEngine; + if (engineToUse == null) { + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); + engineToUse = new MustacheTemplateEngine( + new StandardTemplateResourceResolver( + snippetConfiguration.getFormat())); + } + configuration.put(TemplateEngine.class.getName(), engineToUse); } private void setTemplateEngine(TemplateEngine templateEngine) { @@ -117,8 +124,12 @@ public void apply(Map configuration, RestDocumentationContext context) { WriterResolver resolverToUse = this.writerResolver; if (resolverToUse == null) { + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); resolverToUse = new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver(context)); + new RestDocumentationContextPlaceholderResolver(context), + snippetConfiguration.getEncoding(), + snippetConfiguration.getFormat()); } configuration.put(WriterResolver.class.getName(), resolverToUse); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java new file mode 100644 index 000000000..ecfd94e20 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2016 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.config; + +import org.springframework.restdocs.snippet.SnippetFormat; + +/** + * An encapsulation of the configuration for documentation snippets. + * + * @author Andy Wilkinson + */ +class SnippetConfiguration { + + private final String encoding; + + private final SnippetFormat format; + + SnippetConfiguration(String encoding, SnippetFormat format) { + this.encoding = encoding; + this.format = format; + } + + String getEncoding() { + return this.encoding; + } + + SnippetFormat getFormat() { + return this.format; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 6f4be5a54..a028a7c6f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -24,7 +24,8 @@ import org.springframework.restdocs.curl.CurlDocumentation; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.snippet.SnippetFormats; /** * A configurer that can be used to configure the generated documentation snippets. @@ -51,8 +52,18 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer

    configuration, RestDocumentationContext context) { - ((WriterResolver) configuration.get(WriterResolver.class.getName())) - .setEncoding(this.snippetEncoding); + configuration.put(SnippetConfiguration.class.getName(), new SnippetConfiguration( + this.snippetEncoding, this.snippetFormat)); configuration.put(ATTRIBUTE_DEFAULT_SNIPPETS, this.defaultSnippets); } @@ -93,4 +104,17 @@ public T withDefaults(Snippet... defaultSnippets) { this.defaultSnippets = Arrays.asList(defaultSnippets); return (T) this; } + + /** + * Configures the format of the documentation snippets. + * + * @param format the snippet format + * @return {@code this} + */ + @SuppressWarnings("unchecked") + public T withFormat(SnippetFormat format) { + this.snippetFormat = format; + return (T) this; + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java new file mode 100644 index 000000000..58e31f04e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2016 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; + +/** + * A {@link SnippetFormat} provides information about a particular snippet format, such as + * Asciidoctor or Markdown. + * + * @author Andy Wilkinson + */ +public interface SnippetFormat { + + /** + * Returns the snippet format's file extension. + * + * @return the file extension + */ + String getFileExtension(); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java new file mode 100644 index 000000000..8c24861a4 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java @@ -0,0 +1,74 @@ +/* + * Copyright 2014-2016 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; + +/** + * An enumeration of the built-in snippet formats. + * + * @author Andy Wilkinson + */ +public abstract class SnippetFormats { + + private static final SnippetFormat ASCIIDOCTOR = new AsciidoctorSnippetFormat(); + + private static final SnippetFormat MARKDOWN = new MarkdownSnippetFormat(); + + private SnippetFormats() { + + } + + /** + * Returns the Asciidoctor snippet format. + * + * @return the snippet format + */ + public static SnippetFormat asciidoctor() { + return ASCIIDOCTOR; + } + + /** + * Returns the Markdown snippet format. + * + * @return the snippet format + */ + public static SnippetFormat markdown() { + return MARKDOWN; + } + + private static final class AsciidoctorSnippetFormat implements SnippetFormat { + + private static final String FILE_EXTENSION = "adoc"; + + @Override + public String getFileExtension() { + return FILE_EXTENSION; + } + + } + + private static final class MarkdownSnippetFormat implements SnippetFormat { + + private static final String FILE_EXTENSION = "md"; + + @Override + public String getFileExtension() { + return FILE_EXTENSION; + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 1ef51ea4e..1538da24b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -33,29 +33,54 @@ */ public final class StandardWriterResolver implements WriterResolver { - private String encoding = "UTF-8"; - private final PlaceholderResolver placeholderResolver; private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( "{", "}"); + private String encoding = "UTF-8"; + + private SnippetFormat snippetFormat; + /** * Creates a new {@code StandardWriterResolver} that will use the given * {@code placeholderResolver} to resolve any placeholders in the - * {@code operationName}. + * {@code operationName}. Writers will use {@code UTF-8} encoding and, when writing to + * a file, will use a filename appropriate for Asciidoctor content. * * @param placeholderResolver the placeholder resolver + * @deprecated since 1.1.0 in favor of + * {@link #StandardWriterResolver(PropertyPlaceholderHelper.PlaceholderResolver, String, SnippetFormat)} */ + @Deprecated public StandardWriterResolver(PlaceholderResolver placeholderResolver) { + this(placeholderResolver, "UTF-8", SnippetFormats.asciidoctor()); + } + + /** + * Creates a new {@code StandardWriterResolver} that will use the given + * {@code placeholderResolver} to resolve any placeholders in the + * {@code operationName}. Writers will use the given {@code encoding} and, when + * writing to a file, will use a filename appropriate for content in the given + * {@code snippetFormat}. + * + * @param placeholderResolver the placeholder resolver + * @param encoding the encoding + * @param snippetFormat the snippet format + */ + public StandardWriterResolver(PlaceholderResolver placeholderResolver, + String encoding, SnippetFormat snippetFormat) { this.placeholderResolver = placeholderResolver; + this.encoding = encoding; + this.snippetFormat = snippetFormat; } @Override public Writer resolve(String operationName, String snippetName, RestDocumentationContext context) throws IOException { File outputFile = resolveFile(this.propertyPlaceholderHelper.replacePlaceholders( - operationName, this.placeholderResolver), snippetName + ".adoc", context); + operationName, this.placeholderResolver), snippetName + "." + + this.snippetFormat.getFileExtension(), context); if (outputFile != null) { createDirectoriesIfNecessary(outputFile); @@ -95,4 +120,5 @@ private void createDirectoriesIfNecessary(File outputFile) { throw new IllegalStateException("Failed to create directory '" + parent + "'"); } } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java index 29c2393e3..3caac5ac9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/WriterResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -47,7 +47,10 @@ Writer resolve(String operationName, String snippetName, * resolver. * * @param encoding the encoding + * @deprecated since 1.1.0 in favour of configuring the encoding when to resolver is + * created */ + @Deprecated void setEncoding(String encoding); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index 4e3d38e30..b8b2c62e8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,27 +18,54 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.snippet.SnippetFormats; /** * Standard implementation of {@link TemplateResourceResolver}. *

    - * Templates are resolved by first looking for a resource on the classpath named + * Templates are resolved by looking for a resource on the classpath named * {@code org/springframework/restdocs/templates/{name}.snippet}. If no such - * resource exists {@code default-} is prepended to the name and the classpath is checked - * again. The built-in snippet templates are all named {@code default- name}, thereby - * allowing them to be overridden. + * resource exists an attempt is made to return a default resource that is appropriate for + * the configured snippet format. * * @author Andy Wilkinson */ public class StandardTemplateResourceResolver implements TemplateResourceResolver { + private final SnippetFormat snippetFormat; + + /** + * Creates a new {@code StandardTemplateResourceResolver} that will produce default + * template resources formatted with Asciidoctor. + * + * @deprecated since 1.1.0 in favour of + * {@link #StandardTemplateResourceResolver(SnippetFormat)} + */ + @Deprecated + public StandardTemplateResourceResolver() { + this(SnippetFormats.asciidoctor()); + } + + /** + * Creates a new {@code StandardTemplateResourceResolver} that will produce default + * template resources formatted with the given {@code snippetFormat}. + * + * @param snippetFormat the format for the default snippet templates + */ + public StandardTemplateResourceResolver(SnippetFormat snippetFormat) { + this.snippetFormat = snippetFormat; + } + @Override public Resource resolveTemplateResource(String name) { ClassPathResource classPathResource = new ClassPathResource( "org/springframework/restdocs/templates/" + name + ".snippet"); if (!classPathResource.exists()) { classPathResource = new ClassPathResource( - "org/springframework/restdocs/templates/default-" + name + ".snippet"); + "org/springframework/restdocs/templates/" + + this.snippetFormat.getFileExtension() + "/" + name + + ".snippet"); if (!classPathResource.exists()) { throw new IllegalStateException("Template named '" + name + "' could not be resolved"); diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/default-response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet new file mode 100644 index 000000000..085894270 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet @@ -0,0 +1,3 @@ +```bash +$ curl {{url}} {{options}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet new file mode 100644 index 000000000..9613f5fc7 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet @@ -0,0 +1,7 @@ +```http +{{method}} {{path}} HTTP/1.1 +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{requestBody}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet new file mode 100644 index 000000000..1b01aa5b7 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet @@ -0,0 +1,7 @@ +```http +HTTP/1.1 {{statusCode}} {{statusReason}} +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{responseBody}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet new file mode 100644 index 000000000..116d8eabe --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet @@ -0,0 +1,5 @@ +Relation | Description +-------- | ----------- +{{#links}} +{{rel}} | {{description}} +{{/links}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet new file mode 100644 index 000000000..f5f1daaee --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet @@ -0,0 +1,6 @@ +{{path}} +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet new file mode 100644 index 000000000..e8d2957f3 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet @@ -0,0 +1,5 @@ +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +{{path}} | {{type}} | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet new file mode 100644 index 000000000..f8c5ed27e --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet @@ -0,0 +1,5 @@ +Name | Description +---- | ----------- +{{#headers}} +{{name}} | {{description}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet new file mode 100644 index 000000000..09a8a1992 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet @@ -0,0 +1,5 @@ +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet new file mode 100644 index 000000000..e8d2957f3 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet @@ -0,0 +1,5 @@ +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +{{path}} | {{type}} | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet new file mode 100644 index 000000000..f8c5ed27e --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet @@ -0,0 +1,5 @@ +Name | Description +---- | ----------- +{{#headers}} +{{name}} | {{description}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java new file mode 100644 index 000000000..83017f29d --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2016 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; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; +import org.springframework.restdocs.test.SnippetMatchers; +import org.springframework.restdocs.test.SnippetMatchers.CodeBlockMatcher; +import org.springframework.restdocs.test.SnippetMatchers.HttpRequestMatcher; +import org.springframework.restdocs.test.SnippetMatchers.HttpResponseMatcher; +import org.springframework.restdocs.test.SnippetMatchers.TableMatcher; +import org.springframework.web.bind.annotation.RequestMethod; + +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.snippet.SnippetFormats.markdown; + +/** + * Abstract base class for testing snippet generation. + * + * @author Andy Wilkinson + */ +@RunWith(Parameterized.class) +public abstract class AbstractSnippetTests { + + protected final SnippetFormat snippetFormat; + + @Rule + public ExpectedSnippet snippet; + + @Parameters(name = "{0}") + public static List parameters() { + return Arrays.asList(new Object[] { "Asciidoctor", asciidoctor() }, new Object[] { + "Markdown", markdown() }); + } + + public AbstractSnippetTests(String name, SnippetFormat snippetFormat) { + this.snippet = new ExpectedSnippet(snippetFormat); + this.snippetFormat = snippetFormat; + } + + public CodeBlockMatcher codeBlock(String language) { + return SnippetMatchers.codeBlock(this.snippetFormat, language); + } + + public TableMatcher tableWithHeader(String... headers) { + return SnippetMatchers.tableWithHeader(this.snippetFormat, headers); + } + + public TableMatcher tableWithTitleAndHeader(String title, String... headers) { + return SnippetMatchers + .tableWithTitleAndHeader(this.snippetFormat, title, headers); + } + + public HttpRequestMatcher httpRequest(RequestMethod method, String uri) { + return SnippetMatchers.httpRequest(this.snippetFormat, method, uri); + } + + public HttpResponseMatcher httpResponse(HttpStatus responseStatus) { + return SnippetMatchers.httpResponse(this.snippetFormat, responseStatus); + } + + public OperationBuilder operationBuilder(String name) { + return new OperationBuilder(name, this.snippet.getOutputDirectory(), + this.snippetFormat); + } + + protected FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/" + + this.snippetFormat.getFileExtension() + "/" + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 21c1973d6..f93b04674 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -18,17 +18,16 @@ 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.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; -import org.springframework.restdocs.test.OperationBuilder; import org.springframework.util.Base64Utils; import static org.hamcrest.CoreMatchers.containsString; @@ -36,7 +35,6 @@ import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; /** * Tests for {@link CurlRequestSnippet}. @@ -47,30 +45,27 @@ * @author Jonathan Pearlin * @author Paul-Christian Volkmer */ -public class CurlRequestSnippetTests { +@RunWith(Parameterized.class) +public class CurlRequestSnippetTests extends AbstractSnippetTests { - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(); - - @Rule - public ExpectedException thrown = ExpectedException.none(); + public CurlRequestSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void getRequest() throws IOException { this.snippet.expectCurlRequest("get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - new CurlRequestSnippet().document(new OperationBuilder("get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .build()); + new CurlRequestSnippet().document(operationBuilder("get-request").request( + "http://localhost/foo").build()); } @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - new CurlRequestSnippet().document(new OperationBuilder("non-get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .method("POST").build()); + new CurlRequestSnippet().document(operationBuilder("non-get-request") + .request("http://localhost/foo").method("POST").build()); } @Test @@ -78,9 +73,8 @@ public void requestWithContent() throws IOException { this.snippet.expectCurlRequest("request-with-content").withContents( codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); - new CurlRequestSnippet().document(new OperationBuilder("request-with-content", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .content("content").build()); + new CurlRequestSnippet().document(operationBuilder("request-with-content") + .request("http://localhost/foo").content("content").build()); } @Test @@ -89,8 +83,7 @@ public void getRequestWithQueryString() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document(new OperationBuilder( - "request-with-query-string", this.snippet.getOutputDirectory()).request( + new CurlRequestSnippet().document(operationBuilder("request-with-query-string").request( "http://localhost/foo?param=value").build()); } @@ -99,8 +92,7 @@ public void postRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("post-request-with-query-string").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo?param=value' -i -X POST")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-query-string", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("post-request-with-query-string") .request("http://localhost/foo?param=value").method("POST").build()); } @@ -110,8 +102,7 @@ public void postRequestWithOneParameter() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); new CurlRequestSnippet() - .document(new OperationBuilder("post-request-with-one-parameter", - this.snippet.getOutputDirectory()) + .document(operationBuilder("post-request-with-one-parameter") .request("http://localhost/foo").method("POST").param("k1", "v1") .build()); } @@ -123,10 +114,10 @@ public void postRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-multiple-parameters", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build()); + new CurlRequestSnippet() + .document(operationBuilder("post-request-with-multiple-parameters") + .request("http://localhost/foo").method("POST") + .param("k1", "v1", "v1-bis").param("k2", "v2").build()); } @Test @@ -136,10 +127,10 @@ public void postRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-url-encoded-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1", "a&b").build()); + new CurlRequestSnippet().document(operationBuilder( + "post-request-with-url-encoded-parameter") + .request("http://localhost/foo").method("POST").param("k1", "a&b") + .build()); } @Test @@ -150,10 +141,10 @@ public void postRequestWithQueryStringAndParameter() throws IOException { codeBlock("bash") .content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-query-string-and-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo?a=alpha") - .method("POST").param("b", "bravo").build()); + new CurlRequestSnippet().document(operationBuilder( + "post-request-with-query-string-and-parameter") + .request("http://localhost/foo?a=alpha").method("POST") + .param("b", "bravo").build()); } @Test @@ -165,10 +156,10 @@ public void postRequestWithOverlappingQueryStringAndParameters() throws IOExcept codeBlock("bash") .content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-overlapping-query-string-and-parameters", this.snippet - .getOutputDirectory()).request("http://localhost/foo?a=alpha") - .method("POST").param("a", "alpha").param("b", "bravo").build()); + new CurlRequestSnippet().document(operationBuilder( + "post-request-with-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").method("POST") + .param("a", "alpha").param("b", "bravo").build()); } @Test @@ -176,8 +167,7 @@ public void putRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-one-parameter", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("put-request-with-one-parameter") .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); } @@ -188,11 +178,9 @@ public void putRequestWithMultipleParameters() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet() - .document(new OperationBuilder("put-request-with-multiple-parameters", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); + new CurlRequestSnippet().document(operationBuilder("put-request-with-multiple-parameters") + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); } @Test @@ -201,9 +189,8 @@ public void putRequestWithUrlEncodedParameter() throws IOException { .withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); - new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-url-encoded-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo") + new CurlRequestSnippet().document(operationBuilder( + "put-request-with-url-encoded-parameter").request("http://localhost/foo") .method("PUT").param("k1", "a&b").build()); } @@ -213,8 +200,8 @@ public void requestWithHeaders() throws IOException { codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet().document(new OperationBuilder("request-with-headers", - this.snippet.getOutputDirectory()).request("http://localhost/foo") + new CurlRequestSnippet().document(operationBuilder("request-with-headers") + .request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); } @@ -226,8 +213,7 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { + "'metadata={\"description\": \"foo\"}'"; this.snippet.expectCurlRequest("multipart-post-no-original-filename") .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-no-original-filename", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("multipart-post-no-original-filename") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); @@ -240,8 +226,7 @@ public void multipartPostWithContentType() throws IOException { + "'image=@documents/images/example.png;type=image/png'"; this.snippet.expectCurlRequest("multipart-post-with-content-type").withContents( codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("multipart-post-with-content-type") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -256,9 +241,8 @@ public void multipartPost() throws IOException { + "'image=@documents/images/example.png'"; this.snippet.expectCurlRequest("multipart-post").withContents( codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder("multipart-post", - this.snippet.getOutputDirectory()).request("http://localhost/upload") - .method("POST") + new CurlRequestSnippet().document(operationBuilder("multipart-post") + .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) .submittedFileName("documents/images/example.png").build()); @@ -272,8 +256,7 @@ public void multipartPostWithParameters() throws IOException { + "-F 'b=banana'"; this.snippet.expectCurlRequest("multipart-post-with-parameters").withContents( codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters", this.snippet.getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("multipart-post-with-parameters") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -281,34 +264,30 @@ public void multipartPostWithParameters() throws IOException { .param("a", "apple", "avocado").param("b", "banana").build()); } - @Test - public void customAttributes() throws IOException { - this.snippet.expectCurlRequest("custom-attributes").withContents( - containsString("curl request title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("curl-request")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); - new CurlRequestSnippet(attributes(key("title").value("curl request title"))) - .document(new OperationBuilder("custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost/foo").build()); - } - @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { this.snippet.expectCurlRequest("basic-auth").withContents( codeBlock("bash").content( "$ curl 'http://localhost/foo' -i -u 'user:secret'")); - new CurlRequestSnippet().document(new OperationBuilder("basic-auth", this.snippet - .getOutputDirectory()) + new CurlRequestSnippet().document(operationBuilder("basic-auth") .request("http://localhost/foo") .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils.encodeToString("user:secret".getBytes())) .build()); } + @Test + public void customAttributes() throws IOException { + this.snippet.expectCurlRequest("custom-attributes").withContents( + containsString("curl request title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("curl-request")).willReturn( + snippetResource("curl-request-with-title")); + new CurlRequestSnippet(attributes(key("title").value("curl request title"))) + .document(operationBuilder("custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost/foo").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java new file mode 100644 index 000000000..ca011d2b3 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2016 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.headers; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; + +/** + * Tests for failures when rendering {@link RequestHeadersSnippet} due to missing or + * undocumented headers. + * + * @author Andy Wilkinson + */ +public class RequestHeadersSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void missingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder("missing-request-headers", + this.snippet.getOutputDirectory()).request("http://localhost").build()); + } + + @Test + public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the request: [Accept]")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( + "one"))).document(new OperationBuilder( + "undocumented-request-header-and-missing-request-header", this.snippet + .getOutputDirectory()).request("http://localhost") + .header("X-Test", "test").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index b5b2d1e4c..ed076d263 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,26 +19,20 @@ 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.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link RequestHeadersSnippet}. @@ -46,13 +40,11 @@ * @author Andreas Evers * @author Andy Wilkinson */ -public class RequestHeadersSnippetTests { +public class RequestHeadersSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public RequestHeadersSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void requestWithHeaders() throws IOException { @@ -67,14 +59,13 @@ public void requestWithHeaders() throws IOException { .description("three"), headerWithName("Accept-Language") .description("four"), headerWithName("Cache-Control") .description("five"), - headerWithName("Connection").description("six"))) - .document(new OperationBuilder("request-with-headers", this.snippet - .getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .header("Accept-Encoding", "gzip, deflate") - .header("Accept-Language", "en-US,en;q=0.5") - .header("Cache-Control", "max-age=0") - .header("Connection", "keep-alive").build()); + headerWithName("Connection").description("six"))).document(operationBuilder( + "request-with-headers").request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0").header("Connection", "keep-alive") + .build()); } @Test @@ -83,8 +74,7 @@ public void caseInsensitiveRequestHeaders() throws IOException { .expectRequestHeaders("case-insensitive-request-headers") .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder( - "case-insensitive-request-headers", this.snippet.getOutputDirectory()) + "one"))).document(operationBuilder("case-insensitive-request-headers") .request("/").header("X-test", "test").build()); } @@ -97,37 +87,28 @@ public void undocumentedRequestHeader() throws IOException { } @Test - public void missingRequestHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Headers with the following names were not found" - + " in the request: [Accept]")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( - "one"))).document(new OperationBuilder("missing-request-headers", - this.snippet.getOutputDirectory()).request("http://localhost").build()); - } - - @Test - public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(endsWith("Headers with the following names were not found" - + " in the request: [Accept]")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( - "one"))).document(new OperationBuilder( - "undocumented-request-header-and-missing-request-header", this.snippet - .getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").build()); + public void requestHeadersWithCustomAttributes() throws IOException { + this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") + .withContents(containsString("Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-headers")).willReturn( + snippetResource("request-headers-with-title")); + new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(operationBuilder("request-headers-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").header("X-Test", "test").build()); } @Test public void requestHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") - .withContents(// - tableWithHeader("Name", "Description", "Foo") - .row("X-Test", "one", "alpha") - .row("Accept-Encoding", "two", "bravo") - .row("Accept", "three", "charlie")); + this.snippet.expectRequestHeaders( + "request-headers-with-custom-descriptor-attributes").withContents(// + tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Accept-Encoding", "two", "bravo") + .row("Accept", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")).willReturn( snippetResource("request-headers-with-extra-column")); @@ -137,34 +118,12 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { headerWithName("Accept-Encoding").description("two").attributes( key("foo").value("bravo")), headerWithName("Accept").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "request-headers-with-custom-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("charlie")))).document(operationBuilder( + "request-headers-with-custom-descriptor-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .header("X-Test", "test").header("Accept-Encoding", "gzip, deflate") .header("Accept", "*/*").build()); } - @Test - public void requestHeadersWithCustomAttributes() throws IOException { - this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") - .withContents(startsWith(".Custom title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")).willReturn( - snippetResource("request-headers-with-title")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one")), attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("request-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").header("X-Test", "test").build()); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java new file mode 100644 index 000000000..382e91251 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2016 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.headers; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; + +/** + * Tests for failures when rendering {@link ResponseHeadersSnippet} due to missing or + * undocumented headers. + * + * @author Andy Wilkinson + */ +public class ResponseHeadersSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void missingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "missing-response-headers", this.snippet.getOutputDirectory()).response() + .build()); + } + + @Test + public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(endsWith("Headers with the following names were not found" + + " in the response: [Content-Type]")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") + .description("one"))).document(new OperationBuilder( + "undocumented-response-header-and-missing-response-header", this.snippet + .getOutputDirectory()).response().header("X-Test", "test") + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 24487094f..f58805e01 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,26 +19,20 @@ 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.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link ResponseHeadersSnippet}. @@ -46,13 +40,11 @@ * @author Andreas Evers * @author Andy Wilkinson */ -public class ResponseHeadersSnippetTests { +public class ResponseHeadersSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public ResponseHeadersSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void responseWithHeaders() throws IOException { @@ -65,8 +57,7 @@ public void responseWithHeaders() throws IOException { headerWithName("Content-Type").description("two"), headerWithName("Etag") .description("three"), headerWithName("Cache-Control") .description("five"), headerWithName("Vary").description("six"))) - .document(new OperationBuilder("response-headers", this.snippet - .getOutputDirectory()).response().header("X-Test", "test") + .document(operationBuilder("response-headers").response().header("X-Test", "test") .header("Content-Type", "application/json") .header("Etag", "lskjadldj3ii32l2ij23") .header("Cache-Control", "max-age=0") @@ -79,9 +70,31 @@ public void caseInsensitiveResponseHeaders() throws IOException { .expectResponseHeaders("case-insensitive-response-headers") .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder( - "case-insensitive-response-headers", this.snippet.getOutputDirectory()) - .response().header("X-test", "test").build()); + "one"))).document(operationBuilder("case-insensitive-response-headers").response() + .header("X-test", "test").build()); + } + + @Test + public void undocumentedResponseHeader() throws IOException { + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one"))).document(new OperationBuilder("undocumented-response-header", + this.snippet.getOutputDirectory()).response().header("X-Test", "test") + .header("Content-Type", "*/*").build()); + } + + @Test + public void responseHeadersWithCustomAttributes() throws IOException { + this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + .withContents(containsString("Custom title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-headers")).willReturn( + snippetResource("response-headers-with-title")); + new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( + "one")), attributes(key("title").value("Custom title"))) + .document(operationBuilder("response-headers-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .header("X-Test", "test").build()); } @Test @@ -101,67 +114,12 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { headerWithName("Content-Type").description("two").attributes( key("foo").value("bravo")), headerWithName("Etag").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "response-headers-with-custom-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("charlie")))).document(operationBuilder( + "response-headers-with-custom-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).response() .header("X-Test", "test").header("Content-Type", "application/json") .header("Etag", "lskjadldj3ii32l2ij23").build()); } - @Test - public void responseHeadersWithCustomAttributes() throws IOException { - this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") - .withContents(startsWith(".Custom title")); - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")).willReturn( - snippetResource("response-headers-with-title")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one")), attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("response-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .header("X-Test", "test").build()); - } - - @Test - public void undocumentedResponseHeader() throws IOException { - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder("undocumented-response-header", - this.snippet.getOutputDirectory()).response().header("X-Test", "test") - .header("Content-Type", "*/*").build()); - } - - @Test - public void missingResponseHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Headers with the following names were not found" - + " in the response: [Content-Type]")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") - .description("one"))).document(new OperationBuilder( - "missing-response-headers", this.snippet.getOutputDirectory()).response() - .build()); - } - - @Test - public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(endsWith("Headers with the following names were not found" - + " in the response: [Content-Type]")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") - .description("one"))).document(new OperationBuilder( - "undocumented-response-header-and-missing-response-header", this.snippet - .getOutputDirectory()).response().header("X-Test", "test") - .build()); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index f948fb103..41e90bfb5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,16 +18,14 @@ import java.io.IOException; -import org.junit.Rule; import org.junit.Test; -import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; -import org.springframework.restdocs.test.OperationBuilder; import org.springframework.web.bind.annotation.RequestMethod; import static org.hamcrest.CoreMatchers.containsString; @@ -35,21 +33,20 @@ import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; /** * Tests for {@link HttpRequestSnippet}. * * @author Andy Wilkinson * @author Jonathan Pearlin - * */ -public class HttpRequestSnippetTests { +public class HttpRequestSnippetTests extends AbstractSnippetTests { private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public HttpRequestSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void getRequest() throws IOException { @@ -57,9 +54,8 @@ public void getRequest() throws IOException { httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a").header( HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(new OperationBuilder("get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .header("Alpha", "a").build()); + new HttpRequestSnippet().document(operationBuilder("get-request") + .request("http://localhost/foo").header("Alpha", "a").build()); } @Test @@ -68,8 +64,7 @@ public void getRequestWithQueryString() throws IOException { httpRequest(RequestMethod.GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(new OperationBuilder( - "get-request-with-query-string", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("get-request-with-query-string") .request("http://localhost/foo?bar=baz").build()); } @@ -81,8 +76,7 @@ public void postRequestWithContent() throws IOException { .header(HttpHeaders.HOST, "localhost").content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-content", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("post-request-with-content") .request("http://localhost/foo").method("POST").content(content).build()); } @@ -97,8 +91,7 @@ public void postRequestWithCharset() throws IOException { .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-charset", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("post-request-with-charset") .request("http://localhost/foo").method("POST") .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) .build()); @@ -112,8 +105,7 @@ public void postRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-parameter", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("post-request-with-parameter") .request("http://localhost/foo").method("POST").param("b&r", "baz") .param("a", "alpha").build()); } @@ -126,8 +118,7 @@ public void putRequestWithContent() throws IOException { .header(HttpHeaders.HOST, "localhost").content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(new OperationBuilder( - "put-request-with-content", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("put-request-with-content") .request("http://localhost/foo").method("PUT").content(content).build()); } @@ -139,8 +130,7 @@ public void putRequestWithParameter() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(new OperationBuilder( - "put-request-with-parameter", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("put-request-with-parameter") .request("http://localhost/foo").method("PUT").param("b&r", "baz") .param("a", "alpha").build()); } @@ -154,9 +144,8 @@ public void multipartPost() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder("multipart-post", - this.snippet.getOutputDirectory()).request("http://localhost/upload") - .method("POST") + new HttpRequestSnippet().document(operationBuilder("multipart-post") + .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()).build()); } @@ -177,8 +166,7 @@ public void multipartPostWithParameters() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("multipart-post-with-parameters") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .param("a", "apple", "avocado").param("b", "banana") @@ -195,8 +183,7 @@ public void multipartPostWithContentType() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type", this.snippet.getOutputDirectory()) + new HttpRequestSnippet().document(operationBuilder("multipart-post-with-content-type") .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) @@ -208,8 +195,8 @@ public void getRequestWithCustomHost() throws IOException { this.snippet.expectHttpRequest("get-request-custom-host").withContents( httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, "api.example.com")); - new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host", - this.snippet.getOutputDirectory()).request("http://localhost/foo") + new HttpRequestSnippet().document(operationBuilder("get-request-custom-host") + .request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com").build()); } @@ -218,13 +205,10 @@ public void requestWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents( containsString("Title for the request")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("http-request")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); + given(resolver.resolveTemplateResource("http-request")).willReturn( + snippetResource("http-request-with-title")); new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) - .document(new OperationBuilder("request-with-snippet-attributes", - this.snippet.getOutputDirectory()) + .document(operationBuilder("request-with-snippet-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .request("http://localhost/foo").build()); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 4959e7774..96bc7be41 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -18,24 +18,21 @@ import java.io.IOException; -import org.junit.Rule; import org.junit.Test; -import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; -import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; /** * Tests for {@link HttpResponseSnippet}. @@ -43,25 +40,24 @@ * @author Andy Wilkinson * @author Jonathan Pearlin */ -public class HttpResponseSnippetTests { +public class HttpResponseSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public HttpResponseSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void basicResponse() throws IOException { this.snippet.expectHttpResponse("basic-response").withContents( httpResponse(HttpStatus.OK)); - new HttpResponseSnippet().document(new OperationBuilder("basic-response", - this.snippet.getOutputDirectory()).build()); + new HttpResponseSnippet().document(operationBuilder("basic-response").build()); } @Test public void nonOkResponse() throws IOException { this.snippet.expectHttpResponse("non-ok-response").withContents( httpResponse(HttpStatus.BAD_REQUEST)); - new HttpResponseSnippet().document(new OperationBuilder("non-ok-response", - this.snippet.getOutputDirectory()).response() + new HttpResponseSnippet().document(operationBuilder("non-ok-response").response() .status(HttpStatus.BAD_REQUEST.value()).build()); } @@ -70,8 +66,7 @@ public void responseWithHeaders() throws IOException { this.snippet.expectHttpResponse("response-with-headers").withContents( httpResponse(HttpStatus.OK).header("Content-Type", "application/json") .header("a", "alpha")); - new HttpResponseSnippet().document(new OperationBuilder("response-with-headers", - this.snippet.getOutputDirectory()).response() + new HttpResponseSnippet().document(operationBuilder("response-with-headers").response() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); } @@ -82,8 +77,8 @@ public void responseWithContent() throws IOException { this.snippet.expectHttpResponse("response-with-content").withContents( httpResponse(HttpStatus.OK).content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpResponseSnippet().document(new OperationBuilder("response-with-content", - this.snippet.getOutputDirectory()).response().content(content).build()); + new HttpResponseSnippet().document(operationBuilder("response-with-content").response() + .content(content).build()); } @Test @@ -95,8 +90,7 @@ public void responseWithCharset() throws IOException { .header("Content-Type", "text/plain;charset=UTF-8") .content(japaneseContent) .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); - new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", - this.snippet.getOutputDirectory()).response() + new HttpResponseSnippet().document(operationBuilder("response-with-charset").response() .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) .build()); } @@ -106,13 +100,10 @@ public void responseWithCustomSnippetAttributes() throws IOException { this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( containsString("Title for the response")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("http-response")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); + given(resolver.resolveTemplateResource("http-response")).willReturn( + snippetResource("http-response-with-title")); new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) - .document(new OperationBuilder("response-with-snippet-attributes", - this.snippet.getOutputDirectory()).attribute( + .document(operationBuilder("response-with-snippet-attributes").attribute( TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java new file mode 100644 index 000000000..335127cfb --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-2016 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.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; + +/** + * Tests for failures when rendering {@link LinksSnippet} due to missing or undocumented + * links. + * + * @author Andy Wilkinson + */ +public class LinksSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedLink() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " documented: [foo]")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Collections.emptyList()).document(new OperationBuilder( + "undocumented-link", this.snippet.getOutputDirectory()).build()); + } + + @Test + public void missingLink() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Links with the following relations were not" + + " found in the response: [foo]")); + new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") + .description("bar"))).document(new OperationBuilder("missing-link", + this.snippet.getOutputDirectory()).build()); + } + + @Test + public void undocumentedLinkAndMissingLink() throws IOException { + 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]")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(new OperationBuilder("undocumented-link-and-missing-link", + this.snippet.getOutputDirectory()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 07adce26d..c3bd55a74 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -18,51 +18,29 @@ 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.operation.OperationResponse; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; -import org.springframework.restdocs.test.OperationBuilder; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** * Tests for {@link LinksSnippet}. * * @author Andy Wilkinson */ -public class LinksSnippetTests { +public class LinksSnippetTests extends AbstractSnippetTests { - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(); - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void undocumentedLink() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Links with the following relations were not" - + " documented: [foo]")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), - Collections.emptyList()).document(new OperationBuilder( - "undocumented-link", this.snippet.getOutputDirectory()).build()); + public LinksSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); } @Test @@ -71,19 +49,8 @@ public void ignoredLink() throws IOException { tableWithHeader("Relation", "Description").row("b", "Link b")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(), - new LinkDescriptor("b").description("Link b"))) - .document(new OperationBuilder("ignored-link", this.snippet - .getOutputDirectory()).build()); - } - - @Test - public void missingLink() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Links with the following relations were not" - + " found in the response: [foo]")); - new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") - .description("bar"))).document(new OperationBuilder("missing-link", - this.snippet.getOutputDirectory()).build()); + new LinkDescriptor("b").description("Link b"))).document(operationBuilder( + "ignored-link").build()); } @Test @@ -92,8 +59,7 @@ public void documentedOptionalLink() throws IOException { tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(new OperationBuilder("documented-optional-link", this.snippet - .getOutputDirectory()).build()); + .document(operationBuilder("documented-optional-link").build()); } @Test @@ -101,20 +67,8 @@ public void missingOptionalLink() throws IOException { this.snippet.expectLinks("missing-optional-link").withContents( tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") - .description("bar").optional())).document(new OperationBuilder( - "missing-optional-link", this.snippet.getOutputDirectory()).build()); - } - - @Test - public void undocumentedLinkAndMissingLink() throws IOException { - 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]")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), - Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(new OperationBuilder("undocumented-link-and-missing-link", - this.snippet.getOutputDirectory()).build()); + .description("bar").optional())) + .document(operationBuilder("missing-optional-link").build()); } @Test @@ -125,70 +79,45 @@ public void documentedLinks() throws IOException { 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(new OperationBuilder("documented-links", this.snippet - .getOutputDirectory()).build()); + new LinkDescriptor("b").description("two"))).document(operationBuilder( + "documented-links").build()); } @Test - public void linksWithCustomDescriptorAttributes() throws IOException { + public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("links")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); - this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( - tableWithHeader("Relation", "Description", "Foo") - .row("a", "one", "alpha").row("b", "two", "bravo")); + given(resolver.resolveTemplateResource("links")).willReturn( + snippetResource("links-with-title")); + this.snippet.expectLinks("links-with-custom-attributes").withContents( + containsString("Title for the links")); 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")))).document(new OperationBuilder( - "links-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()).attribute(TemplateEngine.class.getName(), + new LinkDescriptor("a").description("one"), + new LinkDescriptor("b").description("two")), attributes(key("title") + .value("Title for the links"))).document(operationBuilder( + "links-with-custom-attributes").attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); } @Test - public void linksWithCustomAttributes() throws IOException { + public void linksWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("links")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/links-with-title.snippet")); - this.snippet.expectLinks("links-with-custom-attributes").withContents( - startsWith(".Title for the links")); + given(resolver.resolveTemplateResource("links")).willReturn( + snippetResource("links-with-extra-column")); + this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( + tableWithHeader("Relation", "Description", "Foo") + .row("a", "one", "alpha").row("b", "two", "bravo")); 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")), attributes(key("title") - .value("Title for the links"))).document(new OperationBuilder( - "links-with-custom-attributes", this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); - } - - private static class StubLinkExtractor implements LinkExtractor { - - private MultiValueMap linksByRel = new LinkedMultiValueMap<>(); - - @Override - public MultiValueMap extractLinks(OperationResponse response) - throws IOException { - return this.linksByRel; - } - - private StubLinkExtractor withLinks(Link... links) { - for (Link link : links) { - this.linksByRel.add(link.getRel(), link); - } - return this; - } - + new LinkDescriptor("a").description("one").attributes( + key("foo").value("alpha")), + new LinkDescriptor("b").description("two").attributes( + key("foo").value("bravo")))).document(operationBuilder( + "links-with-custom-descriptor-attributes").attribute( + TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java new file mode 100644 index 000000000..237cc5b5c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/StubLinkExtractor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2014-2016 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 org.springframework.restdocs.operation.OperationResponse; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Stub implementation of {@code LinkExtractor} for testing. + * + * @author Andy Wilkinson + */ +class StubLinkExtractor implements LinkExtractor { + + private MultiValueMap linksByRel = new LinkedMultiValueMap<>(); + + @Override + public MultiValueMap extractLinks(OperationResponse response) + throws IOException { + return this.linksByRel; + } + + StubLinkExtractor withLinks(Link... links) { + for (Link link : links) { + this.linksByRel.add(link.getRel(), link); + } + return this; + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java new file mode 100644 index 000000000..a56e8ed5b --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2016 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 java.io.IOException; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.springframework.core.io.FileSystemResource; +import org.springframework.restdocs.snippet.SnippetFormats; +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; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; + +/** + * Tests for {@link RequestFieldsSnippet} that are specific to Asciidoctor. + * + * @author Andy Wilkinson + */ +public class AsciidoctorRequestFieldsSnippetTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Test + public void requestFieldsWithListDescription() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-fields")).willReturn( + snippetResource("request-fields-with-list-description")); + this.snippet.expectRequestFields("request-fields-with-list-description") + .withContents( + tableWithHeader(asciidoctor(), "Path", "Type", "Description") + // + .row("a", "String", String.format(" - one%n - two")) + .configuration("[cols=\"1,1,1a\"]")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description( + Arrays.asList("one", "two")))) + .document(new OperationBuilder("request-fields-with-list-description", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").content("{\"a\": \"foo\"}").build()); + } + + private FileSystemResource snippetResource(String name) { + return new FileSystemResource("src/test/resources/custom-snippet-templates/adoc/" + + name + ".snippet"); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java new file mode 100644 index 000000000..442616b61 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2016 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 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.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for failures when rendering {@link RequestFieldsSnippet} due to missing or + * undocumented fields. + * + * @author Andy Wilkinson + */ +public class RequestFieldsSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @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.emptyList()) + .document(new OperationBuilder("undocumented-request-field", this.snippet + .getOutputDirectory()).request("http://localhost") + .content("{\"a\": 5}").build()); + } + + @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(new OperationBuilder("missing-request-fields", this.snippet + .getOutputDirectory()).request("http://localhost").content("{}") + .build()); + } + + @Test + public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { + this.thrown.expect(FieldTypeRequiredException.class); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .optional())).document(new OperationBuilder( + "missing-optional-request-field-with-no-type", this.snippet + .getOutputDirectory()).request("http://localhost").content("{ }") + .build()); + } + + @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(new OperationBuilder( + "undocumented-request-field-and-missing-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("{ \"a\": { \"c\": 5 }}").build()); + } + + @Test + public void undocumentedXmlRequestField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + new RequestFieldsSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-xml-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void xmlRequestFieldWithNoType() throws IOException { + this.thrown.expect(FieldTypeRequiredException.class); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("missing-xml-request", this.snippet + .getOutputDirectory()) + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void missingXmlRequestField() 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"), + fieldWithPath("a").description("one"))).document(new OperationBuilder( + "missing-xml-request-fields", this.snippet.getOutputDirectory()) + .request("http://localhost").content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void undocumentedXmlRequestFieldAndMissingXmlRequestField() 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(new OperationBuilder( + "undocumented-xml-request-field-and-missing-xml-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 0fedbdee4..7f9a665a6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,43 +18,33 @@ 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.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; -import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; 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; /** * Tests for {@link RequestFieldsSnippet}. * * @author Andy Wilkinson */ -public class RequestFieldsSnippetTests { +public class RequestFieldsSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public RequestFieldsSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void mapRequestWithFields() throws IOException { @@ -65,9 +55,8 @@ public void mapRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), - fieldWithPath("a").description("three"))).document(new OperationBuilder( - "map-request-with-fields", this.snippet.getOutputDirectory()) - .request("http://localhost") + fieldWithPath("a").description("three"))).document(operationBuilder( + "map-request-with-fields").request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @@ -80,24 +69,11 @@ public void arrayRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a") - .description("three"))).document(new OperationBuilder( - "array-request-with-fields", this.snippet.getOutputDirectory()) - .request("http://localhost") + .description("three"))).document(operationBuilder( + "array-request-with-fields").request("http://localhost") .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]").build()); } - @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.emptyList()) - .document(new OperationBuilder("undocumented-request-field", this.snippet - .getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5}").build()); - } - @Test public void ignoredRequestField() throws IOException { this.snippet.expectRequestFields("ignored-request-field").withContents( @@ -105,48 +81,25 @@ public void ignoredRequestField() throws IOException { "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), - fieldWithPath("b").description("Field b"))) - .document(new OperationBuilder("ignored-request-field", this.snippet - .getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5, \"b\": 4}").build()); + fieldWithPath("b").description("Field b"))).document(operationBuilder( + "ignored-request-field").request("http://localhost") + .content("{\"a\": 5, \"b\": 4}").build()); } @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(new OperationBuilder("missing-request-fields", this.snippet - .getOutputDirectory()).request("http://localhost").content("{}") - .build()); - } - - @Test - public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .optional())).document(new OperationBuilder( - "missing-optional-request-field-with-no-type", this.snippet - .getOutputDirectory()).request("http://localhost").content("{ }") - .build()); - } + public void requestFieldsWithCustomAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-fields")).willReturn( + snippetResource("request-fields-with-title")); + this.snippet.expectRequestFields("request-fields-with-custom-attributes") + .withContents(containsString("Custom title")); - @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(new OperationBuilder( - "undocumented-request-field-and-missing-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{ \"a\": { \"c\": 5 }}").build()); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), + attributes(key("title").value("Custom title"))).document(operationBuilder( + "request-fields-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } @Test @@ -167,52 +120,13 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { fieldWithPath("a.c").description("two").attributes( key("foo").value("bravo")), fieldWithPath("a").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "request-fields-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("charlie")))).document(operationBuilder( + "request-fields-with-custom-descriptor-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } - @Test - public void requestFieldsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-title")); - this.snippet.expectRequestFields("request-fields-with-custom-attributes") - .withContents(startsWith(".Custom title")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("request-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}").build()); - } - - @Test - public void requestFieldsWithListDescription() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-list-description")); - this.snippet.expectRequestFields("request-fields-with-list-description") - .withContents( - tableWithHeader("Path", "Type", "Description") - // - .row("a", "String", String.format(" - one%n - two")) - .configuration("[cols=\"1,1,1a\"]")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description( - Arrays.asList("one", "two")))) - .document(new OperationBuilder("request-fields-with-list-description", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}").build()); - } - @Test public void xmlRequestFields() throws IOException { this.snippet.expectRequestFields("xml-request").withContents( @@ -221,78 +135,11 @@ public void xmlRequestFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-request", this.snippet - .getOutputDirectory()) - .request("http://localhost") - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void undocumentedXmlRequestField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - new RequestFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-xml-request-field", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void xmlRequestFieldWithNoType() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("missing-xml-request", this.snippet - .getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void missingXmlRequestField() 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"), - fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-request-fields", this.snippet.getOutputDirectory()) - .request("http://localhost").content("") + fieldWithPath("a").description("three").type("a"))).document(operationBuilder( + "xml-request").request("http://localhost") + .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } - @Test - public void undocumentedXmlRequestFieldAndMissingXmlRequestField() 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(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java new file mode 100644 index 000000000..d664b8074 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2012-2016 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 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.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.endsWith; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for failures when rendering {@link ResponseFieldsSnippet} due to missing or + * undocumented fields. + * + * @author Andy Wilkinson + */ +public class ResponseFieldsSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedXmlResponseField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(startsWith("The following parts of the payload were not" + + " documented:")); + new ResponseFieldsSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-xml-response-field", + this.snippet.getOutputDirectory()) + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void missingXmlAttribute() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/@id]")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type("b"), fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("missing-xml-attribute", this.snippet + .getOutputDirectory()) + .response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void documentedXmlAttributesAreRemoved() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo(String + .format("The following parts of the payload were not documented:" + + "%nbar%n"))); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one") + .type("a"))).document(new OperationBuilder( + "documented-attribute-is-removed", this.snippet.getOutputDirectory()) + .response().content("bar") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void xmlResponseFieldWithNoType() throws IOException { + this.thrown.expect(FieldTypeRequiredException.class); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("xml-response-no-field-type", this.snippet + .getOutputDirectory()) + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void missingXmlResponseField() 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 ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), + fieldWithPath("a").description("one"))).document(new OperationBuilder( + "missing-xml-response-field", this.snippet.getOutputDirectory()) + .response().content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + + @Test + public void undocumentedXmlResponseFieldAndMissingXmlResponseField() + 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 ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) + .document(new OperationBuilder( + "undocumented-xml-request-field-and-missing-xml-request-field", + this.snippet.getOutputDirectory()) + .response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index ad6653d38..f4e00111d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,43 +18,33 @@ 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.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; -import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.endsWith; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; 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; /** * Tests for {@link ResponseFieldsSnippet}. * * @author Andy Wilkinson */ -public class ResponseFieldsSnippetTests { +public class ResponseFieldsSnippetTests extends AbstractSnippetTests { - @Rule - public final ExpectedException thrown = ExpectedException.none(); - - @Rule - public final ExpectedSnippet snippet = new ExpectedSnippet(); + public ResponseFieldsSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); + } @Test public void mapResponseWithFields() throws IOException { @@ -69,14 +59,12 @@ public void mapResponseWithFields() throws IOException { .description("three"), fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), - fieldWithPath("assets[].name").description("six"))) - .document(new OperationBuilder("map-response-with-fields", this.snippet - .getOutputDirectory()) - .response() - .content( - "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" - + " [{\"id\":356,\"name\": \"sample\"}]}") - .build()); + fieldWithPath("assets[].name").description("six"))).document(operationBuilder( + "map-response-with-fields") + .response() + .content( + "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}").build()); } @Test @@ -88,8 +76,7 @@ public void arrayResponseWithFields() throws IOException { new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c") .description("two"), fieldWithPath("[]a").description("three"))) - .document(new OperationBuilder("array-response-with-fields", this.snippet - .getOutputDirectory()).response() + .document(operationBuilder("array-response-with-fields").response() .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") .build()); } @@ -101,8 +88,7 @@ public void arrayResponse() throws IOException { tableWithHeader("Path", "Type", "Description").row("[]", "String", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document(new OperationBuilder("array-response", this.snippet - .getOutputDirectory()).response() + .document(operationBuilder("array-response").response() .content("[\"a\", \"b\", \"c\"]").build()); } @@ -113,10 +99,25 @@ public void ignoredResponseField() throws IOException { "Field b")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), - fieldWithPath("b").description("Field b"))) - .document(new OperationBuilder("ignored-response-field", this.snippet - .getOutputDirectory()).response().content("{\"a\": 5, \"b\": 4}") - .build()); + fieldWithPath("b").description("Field b"))).document(operationBuilder( + "ignored-response-field").response().content("{\"a\": 5, \"b\": 4}") + .build()); + } + + @Test + public void responseFieldsWithCustomAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-fields")).willReturn( + snippetResource("response-fields-with-title")); + this.snippet.expectResponseFields("response-fields-with-custom-attributes") + .withContents(containsString("Custom title")); + + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), + attributes(key("title").value("Custom title"))).document(operationBuilder( + "response-fields-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).response() + .content("{\"a\": \"foo\"}").build()); } @Test @@ -137,31 +138,13 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { fieldWithPath("a.c").description("two").attributes( key("foo").value("bravo")), fieldWithPath("a").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "response-fields-with-custom-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("charlie")))).document(operationBuilder( + "response-fields-with-custom-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).response() .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } - @Test - public void responseFieldsWithCustomAttributes() throws IOException { - TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")).willReturn( - snippetResource("response-fields-with-title")); - this.snippet.expectResponseFields("response-fields-with-custom-attributes") - .withContents(startsWith(".Custom title")); - - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("response-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .content("{\"a\": \"foo\"}").build()); - } - @Test public void xmlResponseFields() throws IOException { this.snippet.expectResponseFields("xml-response").withContents( @@ -169,28 +152,10 @@ public void xmlResponseFields() throws IOException { .row("a/c", "c", "two").row("a", "a", "three")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-response", this.snippet - .getOutputDirectory()) - .response() - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void undocumentedXmlResponseField() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - new ResponseFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-xml-response-field", - this.snippet.getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("three").type("a"))).document(operationBuilder( + "xml-response").response().content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -200,30 +165,13 @@ public void xmlAttribute() throws IOException { "a/@id", "c", "two")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") .type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("xml-attribute", this.snippet - .getOutputDirectory()) + .document(operationBuilder("xml-attribute") .response() .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } - @Test - public void missingXmlAttribute() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/@id]")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("missing-xml-attribute", this.snippet - .getOutputDirectory()) - .response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - @Test public void missingOptionalXmlAttribute() throws IOException { this.snippet.expectResponseFields("missing-optional-xml-attribute").withContents( @@ -231,8 +179,7 @@ public void missingOptionalXmlAttribute() throws IOException { "a/@id", "c", "two")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") .type("b"), fieldWithPath("a/@id").description("two").type("c") - .optional())).document(new OperationBuilder( - "missing-optional-xml-attribute", this.snippet.getOutputDirectory()) + .optional())).document(operationBuilder("missing-optional-xml-attribute") .response().content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); @@ -243,76 +190,10 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { this.snippet.expectResponseFields("undocumented-attribute").withContents( tableWithHeader("Path", "Type", "Description").row("a", "a", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("a"))).document(new OperationBuilder("undocumented-attribute", - this.snippet.getOutputDirectory()).response() + .type("a"))).document(operationBuilder("undocumented-attribute").response() .content("bar") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); } - @Test - public void documentedXmlAttributesAreRemoved() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo(String - .format("The following parts of the payload were not documented:" - + "%nbar%n"))); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one") - .type("a"))).document(new OperationBuilder( - "documented-attribute-is-removed", this.snippet.getOutputDirectory()) - .response().content("bar") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void xmlResponseFieldWithNoType() throws IOException { - this.thrown.expect(FieldTypeRequiredException.class); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("xml-response-no-field-type", this.snippet - .getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void missingXmlResponseField() 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 ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-response-field", this.snippet.getOutputDirectory()) - .response().content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - @Test - public void undocumentedXmlResponseFieldAndMissingXmlResponseField() - 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 ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java new file mode 100644 index 000000000..7f9a07b84 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-2016 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 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.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +/** + * Tests for failures when rendering {@link PathParametersSnippet} due to missing or + * undocumented path parameters. + * + * @author Andy Wilkinson + */ +public class PathParametersSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @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.emptyList()) + .document(new OperationBuilder("undocumented-path-parameter", + this.snippet.getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/").build()); + } + + @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(new OperationBuilder("missing-path-parameter", this.snippet + .getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/").build()); + } + + @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(new OperationBuilder( + "undocumented-and-missing-path-parameters", this.snippet + .getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/{b}").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 4f2ee0b23..9d148769e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -18,78 +18,30 @@ 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.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; -import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; 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.SnippetMatchers.tableWithTitleAndHeader; /** * Tests for {@link PathParametersSnippet}. * * @author Andy Wilkinson - * */ -public class PathParametersSnippetTests { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(); +public class PathParametersSnippetTests extends AbstractSnippetTests { - @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.emptyList()) - .document(new OperationBuilder("undocumented-path-parameter", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/").build()); - } - - @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(new OperationBuilder("missing-path-parameter", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/").build()); - } - - @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(new OperationBuilder( - "undocumented-and-missing-path-parameters", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{b}").build()); + public PathParametersSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); } @Test @@ -99,9 +51,9 @@ public void pathParameters() throws IOException { "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(new OperationBuilder( - "path-parameters", this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + .description("two"))).document(operationBuilder("path-parameters") + .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .build()); } @Test @@ -110,10 +62,9 @@ public void ignoredPathParameter() throws IOException { tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))) - .document(new OperationBuilder("ignored-path-parameter", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + parameterWithName("b").description("two"))).document(operationBuilder( + "ignored-path-parameter").attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); } @Test @@ -124,57 +75,47 @@ public void pathParametersWithQueryString() throws IOException { .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))) - .document(new OperationBuilder("path-parameters-with-query-string", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar") - .build()); + .description("two"))).document(operationBuilder( + "path-parameters-with-query-string").attribute( + "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar").build()); } @Test - public void pathParametersWithCustomDescriptorAttributes() throws IOException { + public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")).willReturn( - snippetResource("path-parameters-with-extra-column")); - this.snippet.expectPathParameters( - "path-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); + snippetResource("path-parameters-with-title")); + this.snippet.expectPathParameters("path-parameters-with-custom-attributes") + .withContents(containsString("The title")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo")))) - .document(new OperationBuilder( - "path-parameters-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + .description("two").attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))).document(operationBuilder( + "path-parameters-with-custom-attributes") + .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); + } @Test - public void pathParametersWithCustomAttributes() throws IOException { + public void pathParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")).willReturn( - snippetResource("path-parameters-with-title")); - this.snippet.expectPathParameters("path-parameters-with-custom-attributes") - .withContents(startsWith(".The title")); + snippetResource("path-parameters-with-extra-column")); + this.snippet.expectPathParameters( + "path-parameters-with-custom-descriptor-attributes").withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", "one", + "alpha").row("b", "two", "bravo")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))) - .document(new OperationBuilder("path-parameters-with-custom-attributes", - this.snippet.getOutputDirectory()) + .description("two").attributes(key("foo").value("bravo")))) + .document(operationBuilder("path-parameters-with-custom-descriptor-attributes") .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); - - } - - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java new file mode 100644 index 000000000..b795dd572 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2016 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 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.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +/** + * Tests for failures when rendering {@link RequestParametersSnippet} due to missing or + * undocumented request parameters. + * + * @author Andy Wilkinson + */ +public class RequestParametersSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Request parameters with the following names were" + + " not documented: [a]")); + new RequestParametersSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-parameter", this.snippet + .getOutputDirectory()).request("http://localhost") + .param("a", "alpha").build()); + } + + @Test + public void missingParameter() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Request parameters with the following names were" + + " not found in the request: [a]")); + new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( + "one"))).document(new OperationBuilder("missing-parameter", this.snippet + .getOutputDirectory()).request("http://localhost").build()); + } + + @Test + public void undocumentedAndMissingParameters() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown + .expectMessage(equalTo("Request parameters with the following names were" + + " not documented: [b]. Request parameters with the following" + + " names were not found in the request: [a]")); + new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( + "one"))).document(new OperationBuilder( + "undocumented-and-missing-parameters", this.snippet.getOutputDirectory()) + .request("http://localhost").param("b", "bravo").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index da61deecc..2facc40c6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -18,75 +18,30 @@ 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.snippet.SnippetException; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.snippet.SnippetFormat; 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; -import org.springframework.restdocs.test.OperationBuilder; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; 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; /** * Tests for {@link RequestParametersSnippet}. * * @author Andy Wilkinson */ -public class RequestParametersSnippetTests { +public class RequestParametersSnippetTests extends AbstractSnippetTests { - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(); - - @Test - public void undocumentedParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Request parameters with the following names were" - + " not documented: [a]")); - new RequestParametersSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-parameter", this.snippet - .getOutputDirectory()).request("http://localhost") - .param("a", "alpha").build()); - } - - @Test - public void missingParameter() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Request parameters with the following names were" - + " not found in the request: [a]")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document(new OperationBuilder("missing-parameter", this.snippet - .getOutputDirectory()).request("http://localhost").build()); - } - - @Test - public void undocumentedAndMissingParameters() throws IOException { - this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Request parameters with the following names were" - + " not documented: [b]. Request parameters with the following" - + " names were not found in the request: [a]")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document(new OperationBuilder( - "undocumented-and-missing-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost").param("b", "bravo").build()); + public RequestParametersSnippetTests(String name, SnippetFormat snippetFormat) { + super(name, snippetFormat); } @Test @@ -96,8 +51,7 @@ public void requestParameters() throws IOException { "two")); new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(new OperationBuilder( - "request-parameters", this.snippet.getOutputDirectory()) + .description("two"))).document(operationBuilder("request-parameters") .request("http://localhost").param("a", "bravo").param("b", "bravo") .build()); } @@ -107,58 +61,50 @@ public void ignoredRequestParameter() throws IOException { this.snippet.expectRequestParameters("ignored-request-parameter").withContents( tableWithHeader("Parameter", "Description").row("b", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))) - .document(new OperationBuilder("ignored-request-parameter", this.snippet - .getOutputDirectory()).request("http://localhost") - .param("a", "bravo").param("b", "bravo").build()); + parameterWithName("b").description("two"))).document(operationBuilder( + "ignored-request-parameter").request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test - public void requestParametersWithCustomDescriptorAttributes() throws IOException { + public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")).willReturn( - snippetResource("request-parameters-with-extra-column")); - this.snippet.expectRequestParameters( - "request-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); + snippetResource("request-parameters-with-title")); + this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") + .withContents(containsString("The title")); new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b").description("two").attributes( - key("foo").value("bravo")))).document(new OperationBuilder( - "request-parameters-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("bravo"))), attributes(key("title").value( + "The title"))).document(operationBuilder( + "request-parameters-with-custom-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .param("a", "alpha").param("b", "bravo").build()); } @Test - public void requestParametersWithCustomAttributes() throws IOException { + public void requestParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")).willReturn( - snippetResource("request-parameters-with-title")); - this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") - .withContents(startsWith(".The title")); + snippetResource("request-parameters-with-extra-column")); + this.snippet.expectRequestParameters( + "request-parameters-with-custom-descriptor-attributes").withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", "one", + "alpha").row("b", "two", "bravo")); new RequestParametersSnippet(Arrays.asList( parameterWithName("a").description("one").attributes( key("foo").value("alpha")), parameterWithName("b").description("two").attributes( - key("foo").value("bravo"))), attributes(key("title").value( - "The title"))).document(new OperationBuilder( - "request-parameters-with-custom-attributes", this.snippet - .getOutputDirectory()) + key("foo").value("bravo")))).document(operationBuilder( + "request-parameters-with-custom-descriptor-attributes") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).request("http://localhost") .param("a", "alpha").param("b", "bravo").build()); } - private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); - } - } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 55ae2218e..31bdb75dc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -26,6 +26,7 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; /** * Tests for {@link StandardWriterResolver}. @@ -37,7 +38,7 @@ public class StandardWriterResolverTests { private final PlaceholderResolver placeholderResolver = mock(PlaceholderResolver.class); private final StandardWriterResolver resolver = new StandardWriterResolver( - this.placeholderResolver); + this.placeholderResolver, "UTF-8", asciidoctor()); @Test public void noConfiguredOutputDirectoryAndRelativeInput() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index ef807ee20..00a3c8d4a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -29,6 +29,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; /** * Tests for {@link TemplateResourceResolver}. @@ -40,7 +41,8 @@ public class StandardTemplateResourceResolverTests { @Rule public final ExpectedException thrown = ExpectedException.none(); - private final TemplateResourceResolver resolver = new StandardTemplateResourceResolver(); + private final TemplateResourceResolver resolver = new StandardTemplateResourceResolver( + asciidoctor()); private final TestClassLoader classLoader = new TestClassLoader(); @@ -66,7 +68,7 @@ public Resource call() { @Test public void fallsBackToDefaultSnippet() throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/default-test.snippet", getClass() + "org/springframework/restdocs/templates/adoc/test.snippet", getClass() .getResource("test.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 63364f6a6..6a571f350 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -23,6 +23,7 @@ import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; @@ -38,14 +39,21 @@ */ public class ExpectedSnippet implements TestRule { + private final SnippetFormat snippetFormat; + + private final SnippetMatcher snippet; + private String expectedName; private String expectedType; - private SnippetMatcher snippet = SnippetMatchers.snippet(); - private File outputDirectory; + public ExpectedSnippet(SnippetFormat snippetFormat) { + this.snippetFormat = snippetFormat; + this.snippet = SnippetMatchers.snippet(snippetFormat); + } + @Override public Statement apply(final Statement base, Description description) { this.outputDirectory = new File("build/" @@ -56,7 +64,8 @@ public Statement apply(final Statement base, Description description) { private void verifySnippet() throws IOException { if (this.outputDirectory != null && this.expectedName != null) { File snippetDir = new File(this.outputDirectory, this.expectedName); - File snippetFile = new File(snippetDir, this.expectedType + ".adoc"); + File snippetFile = new File(snippetDir, this.expectedType + "." + + this.snippetFormat.getFileExtension()); assertThat(snippetFile, is(this.snippet)); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index e5be81665..e2a54b998 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -37,6 +37,8 @@ import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.StandardOperation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; @@ -58,11 +60,18 @@ public class OperationBuilder { private final File outputDirectory; + private final SnippetFormat snippetFormat; + private OperationRequestBuilder requestBuilder; public OperationBuilder(String name, File outputDirectory) { + this(name, outputDirectory, SnippetFormats.asciidoctor()); + } + + public OperationBuilder(String name, File outputDirectory, SnippetFormat snippetFormat) { this.name = name; this.outputDirectory = outputDirectory; + this.snippetFormat = snippetFormat; } public OperationRequestBuilder request(String uri) { @@ -82,13 +91,15 @@ public OperationBuilder attribute(String name, Object value) { public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { this.attributes.put(TemplateEngine.class.getName(), - new MustacheTemplateEngine(new StandardTemplateResourceResolver())); + new MustacheTemplateEngine(new StandardTemplateResourceResolver( + this.snippetFormat))); } RestDocumentationContext context = new RestDocumentationContext(null, null, this.outputDirectory); this.attributes.put(RestDocumentationContext.class.getName(), context); this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver(context))); + new RestDocumentationContextPlaceholderResolver(context), "UTF-8", + this.snippetFormat)); return new StandardOperation(this.name, (this.requestBuilder == null ? new OperationRequestBuilder( "http://localhost/").buildRequest() : this.requestBuilder diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 97818a52e..c076d0ca7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -30,6 +30,8 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.springframework.http.HttpStatus; +import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; @@ -45,37 +47,62 @@ private SnippetMatchers() { } - public static SnippetMatcher snippet() { - return new SnippetMatcher(); + public static SnippetMatcher snippet(SnippetFormat snippetFormat) { + return new SnippetMatcher(snippetFormat); } - public static AsciidoctorTableMatcher tableWithTitleAndHeader(String title, - String... headers) { - return new AsciidoctorTableMatcher(title, headers); + public static TableMatcher tableWithHeader(SnippetFormat format, String... headers) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorTableMatcher(null, headers); + } + return new MarkdownTableMatcher(null, headers); } - public static AsciidoctorTableMatcher tableWithHeader(String... headers) { - return new AsciidoctorTableMatcher(null, headers); + public static TableMatcher tableWithTitleAndHeader(SnippetFormat format, + String title, String... headers) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorTableMatcher(title, headers); + } + return new MarkdownTableMatcher(title, headers); } - public static HttpRequestMatcher httpRequest(RequestMethod method, String uri) { - return new HttpRequestMatcher(method, uri); + public static HttpRequestMatcher httpRequest(SnippetFormat format, + RequestMethod requestMethod, String uri) { + if ("adoc".equals(format.getFileExtension())) { + return new HttpRequestMatcher(requestMethod, uri, + new AsciidoctorCodeBlockMatcher<>("http"), 3); + } + return new HttpRequestMatcher(requestMethod, uri, new MarkdownCodeBlockMatcher<>( + "http"), 2); } - public static HttpResponseMatcher httpResponse(HttpStatus status) { - return new HttpResponseMatcher(status); + public static HttpResponseMatcher httpResponse(SnippetFormat format, HttpStatus status) { + if ("adoc".equals(format.getFileExtension())) { + return new HttpResponseMatcher(status, new AsciidoctorCodeBlockMatcher<>( + "http"), 3); + } + return new HttpResponseMatcher(status, new MarkdownCodeBlockMatcher<>("http"), 2); } @SuppressWarnings({ "rawtypes" }) - public static AsciidoctorCodeBlockMatcher codeBlock(String language) { - return new AsciidoctorCodeBlockMatcher(language); + public static CodeBlockMatcher codeBlock(SnippetFormat format, String language) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorCodeBlockMatcher(language); + } + return new MarkdownCodeBlockMatcher(language); } private static abstract class AbstractSnippetContentMatcher extends BaseMatcher { + private final SnippetFormat snippetFormat; + private List lines = new ArrayList<>(); + protected AbstractSnippetContentMatcher(SnippetFormat snippetFormat) { + this.snippetFormat = snippetFormat; + } + protected void addLine(String line) { this.lines.add(line); } @@ -94,7 +121,7 @@ public boolean matches(Object item) { @Override public void describeTo(Description description) { - description.appendText("Asciidoctor snippet"); + description.appendText(this.snippetFormat.getFileExtension() + " snippet"); description.appendText(getLinesAsString()); } @@ -122,24 +149,55 @@ private String getLinesAsString() { } } + /** + * Base class for code block matchers. + * + * @param The type of the matcher + */ + public static class CodeBlockMatcher> extends + AbstractSnippetContentMatcher { + + protected CodeBlockMatcher(SnippetFormat snippetFormat) { + super(snippetFormat); + } + + @SuppressWarnings("unchecked") + public T content(String content) { + this.addLine(-1, content); + return (T) this; + } + + } + /** * A {@link Matcher} for an Asciidoctor code block. * * @param The type of the matcher */ public static class AsciidoctorCodeBlockMatcher> - extends AbstractSnippetContentMatcher { + extends CodeBlockMatcher { protected AsciidoctorCodeBlockMatcher(String language) { + super(SnippetFormats.asciidoctor()); this.addLine("[source," + language + "]"); this.addLine("----"); this.addLine("----"); } - @SuppressWarnings("unchecked") - public T content(String content) { - this.addLine(-1, content); - return (T) this; + } + + /** + * A {@link Matcher} for a Markdown code block. + * + * @param The type of the matcher + */ + public static class MarkdownCodeBlockMatcher> + extends CodeBlockMatcher { + + protected MarkdownCodeBlockMatcher(String language) { + super(SnippetFormats.markdown()); + this.addLine("```" + language); + this.addLine("```"); } } @@ -150,26 +208,45 @@ public T content(String content) { * @param The type of the matcher */ public static abstract class HttpMatcher> extends - AsciidoctorCodeBlockMatcher> { + BaseMatcher { + + private final CodeBlockMatcher delegate; - private int headerOffset = 3; + private int headerOffset; - protected HttpMatcher() { - super("http"); + protected HttpMatcher(CodeBlockMatcher delegate, int headerOffset) { + this.delegate = delegate; + this.headerOffset = headerOffset; } @SuppressWarnings("unchecked") public T header(String name, String value) { - this.addLine(this.headerOffset++, name + ": " + value); + this.delegate.addLine(this.headerOffset++, name + ": " + value); return (T) this; } @SuppressWarnings("unchecked") public T header(String name, long value) { - this.addLine(this.headerOffset++, name + ": " + value); + this.delegate.addLine(this.headerOffset++, name + ": " + value); return (T) this; } + @SuppressWarnings("unchecked") + public T content(String content) { + this.delegate.addLine(-1, content); + return (T) this; + } + + @Override + public boolean matches(Object item) { + return this.delegate.matches(item); + } + + @Override + public void describeTo(Description description) { + this.delegate.describeTo(description); + } + } /** @@ -178,7 +255,9 @@ public T header(String name, long value) { public static final class HttpResponseMatcher extends HttpMatcher { - private HttpResponseMatcher(HttpStatus status) { + private HttpResponseMatcher(HttpStatus status, CodeBlockMatcher delegate, + int headerOffset) { + super(delegate, headerOffset); this.content("HTTP/1.1 " + status.value() + " " + status.getReasonPhrase()); this.content(""); } @@ -190,20 +269,41 @@ private HttpResponseMatcher(HttpStatus status) { */ public static final class HttpRequestMatcher extends HttpMatcher { - private HttpRequestMatcher(RequestMethod requestMethod, String uri) { + private HttpRequestMatcher(RequestMethod requestMethod, String uri, + CodeBlockMatcher delegate, int headerOffset) { + super(delegate, headerOffset); this.content(requestMethod.name() + " " + uri + " HTTP/1.1"); this.content(""); } } + /** + * Base class for table matchers. + * + * @param The concrete type of the matcher + */ + public static abstract class TableMatcher> extends + AbstractSnippetContentMatcher { + + protected TableMatcher(SnippetFormat snippetFormat) { + super(snippetFormat); + } + + public abstract T row(String... entries); + + public abstract T configuration(String configuration); + + } + /** * A {@link Matcher} for an Asciidoctor table. */ public static final class AsciidoctorTableMatcher extends - AbstractSnippetContentMatcher { + TableMatcher { private AsciidoctorTableMatcher(String title, String... columns) { + super(SnippetFormats.asciidoctor()); if (StringUtils.hasText(title)) { this.addLine("." + title); } @@ -216,6 +316,7 @@ private AsciidoctorTableMatcher(String title, String... columns) { this.addLine("|==="); } + @Override public AsciidoctorTableMatcher row(String... entries) { for (String entry : entries) { this.addLine(-1, "|" + entry); @@ -224,19 +325,68 @@ public AsciidoctorTableMatcher row(String... entries) { return this; } + @Override public AsciidoctorTableMatcher configuration(String configuration) { this.addLine(0, configuration); return this; } + + } + + /** + * A {@link Matcher} for a Markdown table. + */ + public static final class MarkdownTableMatcher extends + TableMatcher { + + private MarkdownTableMatcher(String title, String... columns) { + super(SnippetFormats.asciidoctor()); + if (StringUtils.hasText(title)) { + this.addLine(title); + } + String header = StringUtils.collectionToDelimitedString( + Arrays.asList(columns), " | "); + this.addLine(header); + List components = new ArrayList<>(); + for (String column : columns) { + StringBuilder dashes = new StringBuilder(); + for (int i = 0; i < column.length(); i++) { + dashes.append("-"); + } + components.add(dashes.toString()); + } + this.addLine(StringUtils.collectionToDelimitedString(components, " | ")); + this.addLine(""); + } + + @Override + public MarkdownTableMatcher row(String... entries) { + this.addLine(-1, StringUtils.collectionToDelimitedString( + Arrays.asList(entries), " | ")); + return this; + } + + @Override + public MarkdownTableMatcher configuration(String configuration) { + throw new UnsupportedOperationException( + "Markdown does not support table configuration"); + } + } /** * A {@link Matcher} for a snippet file. */ - public static class SnippetMatcher extends BaseMatcher { + public static final class SnippetMatcher extends BaseMatcher { + + private final SnippetFormat snippetFormat; private Matcher expectedContents; + private SnippetMatcher(SnippetFormat snippetFormat) { + this.snippetFormat = snippetFormat; + } + @Override public boolean matches(Object item) { if (snippetFileExists(item)) { @@ -285,7 +435,8 @@ public void describeTo(Description description) { this.expectedContents.describeTo(description); } else { - description.appendText("Asciidoctor snippet"); + description + .appendText(this.snippetFormat.getFileExtension() + " snippet"); } } @@ -295,4 +446,5 @@ public SnippetMatcher withContents(Matcher matcher) { } } + } diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/curl-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/curl-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/curl-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/http-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/http-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/http-response-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-response-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/http-response-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-response-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/links-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/path-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-list-description.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-list-description.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-list-description.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-list-description.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/response-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/response-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet new file mode 100644 index 000000000..483ab6c7f --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet @@ -0,0 +1,4 @@ +{{title}} +```bash +$ curl {{url}} {{options}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet new file mode 100644 index 000000000..363a93694 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet @@ -0,0 +1,8 @@ +{{title}} +```http +{{method}} {{path}} HTTP/1.1 +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{requestBody}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet new file mode 100644 index 000000000..103fa6dcb --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet @@ -0,0 +1,8 @@ +{{title}} +```http +HTTP/1.1 {{statusCode}} {{statusReason}} +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{responseBody}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet new file mode 100644 index 000000000..a7e3349d3 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet @@ -0,0 +1,5 @@ +Relation | Description | Foo +-------- | ----------- | --- +{{#links}} +{{rel}} | {{description}} | {{foo}} +{{/links}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet new file mode 100644 index 000000000..ecaaeb33b --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Relation | Description +-------- | ----------- +{{#links}} +{{rel}} | {{description}} +{{/links}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet new file mode 100644 index 000000000..260502b69 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet @@ -0,0 +1,5 @@ +Parameter | Description | Foo +--------- | ----------- | --- +{{#parameters}} +{{name}} | {{description}} | {{foo}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet new file mode 100644 index 000000000..b63b62353 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet new file mode 100644 index 000000000..e4ec4c605 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet @@ -0,0 +1,5 @@ +Path | Type | Description | Foo +---- | ---- | ----------- | --- +{{#fields}} +{{path}} | {{type}} | {{description}} | {{foo}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet new file mode 100644 index 000000000..24bb63fa9 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +{{path}} | {{type}} | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet new file mode 100644 index 000000000..af392f2a8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet @@ -0,0 +1,5 @@ +Name | Description | Foo +---- | ----------- | --- +{{#headers}} +{{name}} | {{description}} | {{foo}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet new file mode 100644 index 000000000..d57ed7ba8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Name | Description +---- | ----------- +{{#headers}} +{{name}} | {{description}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet new file mode 100644 index 000000000..260502b69 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet @@ -0,0 +1,5 @@ +Parameter | Description | Foo +--------- | ----------- | --- +{{#parameters}} +{{name}} | {{description}} | {{foo}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet new file mode 100644 index 000000000..b63b62353 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Parameter | Description +--------- | ----------- +{{#parameters}} +{{name}} | {{description}} +{{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet new file mode 100644 index 000000000..e4ec4c605 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet @@ -0,0 +1,5 @@ +Path | Type | Description | Foo +---- | ---- | ----------- | --- +{{#fields}} +{{path}} | {{type}} | {{description}} | {{foo}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet new file mode 100644 index 000000000..24bb63fa9 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +{{path}} | {{type}} | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet new file mode 100644 index 000000000..af392f2a8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet @@ -0,0 +1,5 @@ +Name | Description | Foo +---- | ----------- | --- +{{#headers}} +{{name}} | {{description}} | {{foo}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet new file mode 100644 index 000000000..d57ed7ba8 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Name | Description +---- | ----------- +{{#headers}} +{{name}} | {{description}} +{{/headers}} \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index f9beacace..bb7d7071c 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -67,8 +67,8 @@ public UriConfigurer uris() { public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation, - Arrays.asList(getTemplateEngineConfigurer(), - getWriterResolverConfigurer(), snippets(), this.uriConfigurer)); + Arrays.asList(snippets(), getTemplateEngineConfigurer(), + getWriterResolverConfigurer(), this.uriConfigurer)); } @Override diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index d444dd177..0c48c4801 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -75,6 +75,8 @@ import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.snippet.SnippetFormats.markdown; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; @@ -123,6 +125,20 @@ public void basicSnippetGeneration() throws Exception { "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } + @Test + public void markdownSnippetGeneration() throws Exception { + MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation) + .snippets().withEncoding("UTF-8").withFormat(markdown())).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("basic-markdown")); + assertExpectedSnippetFilesExist(new File( + "build/generated-snippets/basic-markdown"), "http-request.md", + "http-response.md", "curl-request.md"); + } + @Test public void curlSnippetWithContent() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -130,10 +146,11 @@ public void curlSnippetWithContent() throws Exception { mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) .andExpect(status().isOk()).andDo(document("curl-snippet-with-content")); - assertThat(new File( - "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), + is(snippet(asciidoctor()).withContents( + codeBlock(asciidoctor(), "bash").content( "$ curl " + "'http://localhost:8080/' -i -X POST " + "-H 'Accept: application/json' -d 'content'")))); } @@ -150,8 +167,8 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { assertThat( new File( "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( + is(snippet(asciidoctor()).withContents( + codeBlock(asciidoctor(), "bash").content( "$ curl " + "'http://localhost:8080/?foo=bar' -i -X POST " + "-H 'Accept: application/json' -d 'a=alpha'")))); @@ -298,9 +315,9 @@ public void preprocessedRequest() throws Exception { assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("a", "alpha") - .header("b", "bravo") + is(snippet(asciidoctor()).withContents( + httpRequest(asciidoctor(), RequestMethod.GET, "/") + .header("a", "alpha").header("b", "bravo") .header("Content-Type", "application/json") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .header("Host", "localhost") @@ -310,8 +327,9 @@ public void preprocessedRequest() throws Exception { assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("b", "bravo") + is(snippet(asciidoctor()).withContents( + httpRequest(asciidoctor(), RequestMethod.GET, "/") + .header("b", "bravo") .header("Content-Type", "application/json") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .content(prettyPrinted)))); @@ -337,8 +355,8 @@ public void preprocessedResponse() throws Exception { + "\"href\":\"href\"}]}"; assertThat( new File("build/generated-snippets/original-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) + is(snippet(asciidoctor()).withContents( + httpResponse(asciidoctor(), HttpStatus.OK) .header("a", "alpha") .header("Content-Type", "application/json") .header(HttpHeaders.CONTENT_LENGTH, @@ -348,8 +366,8 @@ public void preprocessedResponse() throws Exception { assertThat( new File( "build/generated-snippets/preprocessed-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) + is(snippet(asciidoctor()).withContents( + httpResponse(asciidoctor(), HttpStatus.OK) .header("Content-Type", "application/json") .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length) @@ -376,7 +394,7 @@ public void customSnippetTemplate() throws Exception { } assertThat(new File( "build/generated-snippets/custom-snippet-template/curl-request.adoc"), - is(snippet().withContents(equalTo("Custom curl request")))); + is(snippet(asciidoctor()).withContents(equalTo("Custom curl request")))); mockMvc.perform(get("/")).andDo( document( @@ -395,9 +413,9 @@ public void customContextPath() throws Exception { .andExpect(status().isOk()).andDo(document("custom-context-path")); assertThat( new File("build/generated-snippets/custom-context-path/curl-request.adoc"), - is(snippet() + is(snippet(asciidoctor()) .withContents( - codeBlock("bash") + codeBlock(asciidoctor(), "bash") .content( "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 5dbdb1c09..256d65271 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -51,8 +51,8 @@ public final class RestAssuredRestDocumentationConfigurer RestAssuredRestDocumentationConfigurer(RestDocumentation restDocumentation) { this.restDocumentation = restDocumentation; - this.configurers = Arrays.asList(getTemplateEngineConfigurer(), - getWriterResolverConfigurer(), snippets()); + this.configurers = Arrays.asList(snippets(), getTemplateEngineConfigurer(), + getWriterResolverConfigurer()); } @Override diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 3ae6d13f5..eeea60b48 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -70,6 +70,7 @@ import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; +import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; @@ -114,8 +115,8 @@ public void curlSnippetWithContent() throws Exception { assertThat( new File( "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( + is(snippet(asciidoctor()).withContents( + codeBlock(asciidoctor(), "bash").content( "$ curl 'http://localhost:" + this.port + "/' -i " + "-X POST -H 'Accept: application/json' " + "-H 'Content-Type: " + contentType + "' " @@ -133,8 +134,8 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { assertThat( new File( "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( + is(snippet(asciidoctor()).withContents( + codeBlock(asciidoctor(), "bash").content( "$ curl " + "'http://localhost:" + this.port + "/?foo=bar' -i -X POST " + "-H 'Accept: application/json' " @@ -261,9 +262,9 @@ public void preprocessedRequest() throws Exception { .then().statusCode(200); assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet() + is(snippet(asciidoctor()) .withContents( - httpRequest(RequestMethod.GET, "/") + httpRequest(asciidoctor(), RequestMethod.GET, "/") .header("a", "alpha") .header("b", "bravo") .header("Accept", @@ -277,9 +278,9 @@ public void preprocessedRequest() throws Exception { assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet() + is(snippet(asciidoctor()) .withContents( - httpRequest(RequestMethod.GET, "/") + httpRequest(asciidoctor(), RequestMethod.GET, "/") .header("b", "bravo") .header("Accept", MediaType.APPLICATION_JSON_VALUE) @@ -308,8 +309,8 @@ public void preprocessedResponse() throws Exception { assertThat( new File( "build/generated-snippets/preprocessed-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) + is(snippet(asciidoctor()).withContents( + httpResponse(asciidoctor(), HttpStatus.OK) .header("Foo", "https://api.example.com/foo/bar") .header("Content-Type", "application/json;charset=UTF-8") .header(HttpHeaders.CONTENT_LENGTH, @@ -335,7 +336,7 @@ public void customSnippetTemplate() throws Exception { } assertThat(new File( "build/generated-snippets/custom-snippet-template/curl-request.adoc"), - is(snippet().withContents(equalTo("Custom curl request")))); + is(snippet(asciidoctor()).withContents(equalTo("Custom curl request")))); } private void assertExpectedSnippetFilesExist(File directory, String... snippets) { From 28476c2d08aae9a139f1ae152fe374e2cb1b6bd7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 2 Feb 2016 14:09:20 +0000 Subject: [PATCH 050/898] Polish RestDocumentationConfigurer and improve its test coverage --- .../config/RestDocumentationConfigurer.java | 28 +-- .../RestDocumentationConfigurerTests.java | 180 ++++++++++++++++++ .../MockMvcRestDocumentationConfigurer.java | 21 +- ...estAssuredRestDocumentationConfigurer.java | 11 +- 4 files changed, 201 insertions(+), 39 deletions(-) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 0f0ff9b21..ee721725b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.config; +import java.util.Arrays; +import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; @@ -34,7 +36,7 @@ * support chaining * @author Andy Wilkinson */ -public abstract class RestDocumentationConfigurer { +public abstract class RestDocumentationConfigurer { private final WriterResolverConfigurer writerResolverConfigurer = new WriterResolverConfigurer(); @@ -74,21 +76,19 @@ public final T writerResolver(WriterResolver writerResolver) { } /** - * Returns the configurer used to configure the context with a {@link WriterResolver}. + * Applies this configurer to the given {@code configuration} within the given + * {@code context}. * - * @return the configurer + * @param configuration the configuration + * @param context the current context */ - protected final AbstractConfigurer getWriterResolverConfigurer() { - return this.writerResolverConfigurer; - } - - /** - * Returns the configurer used to configure the context with a {@link TemplateEngine}. - * - * @return the configurer - */ - protected final AbstractConfigurer getTemplateEngineConfigurer() { - return this.templateEngineConfigurer; + protected final void apply(Map configuration, + RestDocumentationContext context) { + List configurers = Arrays.asList(snippets(), + this.templateEngineConfigurer, this.writerResolverConfigurer); + for (AbstractConfigurer configurer : configurers) { + configurer.apply(configuration, context); + } } private static final class TemplateEngineConfigurer extends AbstractConfigurer { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java new file mode 100644 index 000000000..2dad00fe9 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -0,0 +1,180 @@ +/* + * Copyright 2012-2016 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.config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.curl.CurlRequestSnippet; +import org.springframework.restdocs.http.HttpRequestSnippet; +import org.springframework.restdocs.http.HttpResponseSnippet; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.snippet.StandardWriterResolver; +import org.springframework.restdocs.snippet.WriterResolver; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RestDocumentationConfigurer}. + * + * @author Andy Wilkinson + */ +public class RestDocumentationConfigurerTests { + + private final TestRestDocumentationConfigurer configurer = new TestRestDocumentationConfigurer(); + + @SuppressWarnings("unchecked") + @Test + public void defaultConfiguration() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + this.configurer.apply(configuration, context); + assertThat( + configuration, + hasEntry(equalTo(TemplateEngine.class.getName()), + instanceOf(MustacheTemplateEngine.class))); + assertThat( + configuration, + hasEntry(equalTo(WriterResolver.class.getName()), + instanceOf(StandardWriterResolver.class))); + assertThat( + configuration, + hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + instanceOf(List.class))); + List defaultSnippets = (List) configuration + .get("org.springframework.restdocs.defaultSnippets"); + assertThat( + defaultSnippets, + contains(instanceOf(CurlRequestSnippet.class), + instanceOf(HttpRequestSnippet.class), + instanceOf(HttpResponseSnippet.class))); + assertThat( + configuration, + hasEntry(equalTo(SnippetConfiguration.class.getName()), + instanceOf(SnippetConfiguration.class))); + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); + assertThat(snippetConfiguration.getEncoding(), is(equalTo("UTF-8"))); + assertThat(snippetConfiguration.getFormat(), + is(equalTo(SnippetFormats.asciidoctor()))); + } + + @Test + public void customTemplateEngine() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + TemplateEngine templateEngine = mock(TemplateEngine.class); + this.configurer.templateEngine(templateEngine).apply(configuration, context); + assertThat(configuration, Matchers.hasEntry( + TemplateEngine.class.getName(), templateEngine)); + } + + @Test + public void customWriterResolver() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + WriterResolver writerResolver = mock(WriterResolver.class); + this.configurer.writerResolver(writerResolver).apply(configuration, context); + assertThat(configuration, Matchers.hasEntry( + WriterResolver.class.getName(), writerResolver)); + } + + @Test + public void customDefaultSnippets() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + this.configurer.snippets().withDefaults(CurlDocumentation.curlRequest()) + .apply(configuration, context); + assertThat( + configuration, + hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + instanceOf(List.class))); + @SuppressWarnings("unchecked") + List defaultSnippets = (List) configuration + .get("org.springframework.restdocs.defaultSnippets"); + assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class))); + } + + @Test + public void customSnippetEncoding() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + this.configurer.snippets().withEncoding("ISO 8859-1") + .apply(configuration, context); + assertThat( + configuration, + hasEntry(equalTo(SnippetConfiguration.class.getName()), + instanceOf(SnippetConfiguration.class))); + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); + assertThat(snippetConfiguration.getEncoding(), is(equalTo("ISO 8859-1"))); + } + + @Test + public void customSnippetFormat() { + RestDocumentationContext context = new RestDocumentationContext(null, null, null); + Map configuration = new HashMap<>(); + this.configurer.snippets().withFormat(SnippetFormats.markdown()) + .apply(configuration, context); + assertThat( + configuration, + hasEntry(equalTo(SnippetConfiguration.class.getName()), + instanceOf(SnippetConfiguration.class))); + SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration + .get(SnippetConfiguration.class.getName()); + assertThat(snippetConfiguration.getFormat(), + is(equalTo(SnippetFormats.markdown()))); + } + + private static final class TestRestDocumentationConfigurer + extends + RestDocumentationConfigurer { + + private final TestSnippetConfigurer snippetConfigurer = new TestSnippetConfigurer( + this); + + @Override + public TestSnippetConfigurer snippets() { + return this.snippetConfigurer; + } + + } + + private static final class TestSnippetConfigurer extends + SnippetConfigurer { + + private TestSnippetConfigurer(TestRestDocumentationConfigurer parent) { + super(parent); + } + + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index bb7d7071c..a402487fe 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -16,15 +16,12 @@ package org.springframework.restdocs.mockmvc; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.config.AbstractConfigurer; import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; @@ -66,9 +63,7 @@ public UriConfigurer uris() { @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation, - Arrays.asList(snippets(), getTemplateEngineConfigurer(), - getWriterResolverConfigurer(), this.uriConfigurer)); + return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation); } @Override @@ -81,17 +76,13 @@ public MockMvcSnippetConfigurer snippets() { return this.snippetConfigurer; } - private static final class ConfigurerApplyingRequestPostProcessor implements + private final class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { private final RestDocumentation restDocumentation; - private final List configurers; - - private ConfigurerApplyingRequestPostProcessor( - RestDocumentation restDocumentation, List configurers) { + private ConfigurerApplyingRequestPostProcessor(RestDocumentation restDocumentation) { this.restDocumentation = restDocumentation; - this.configurers = configurers; } @Override @@ -105,9 +96,9 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) request.getAttribute(urlTemplateAttribute)); request.setAttribute("org.springframework.restdocs.configuration", configuration); - for (AbstractConfigurer configurer : this.configurers) { - configurer.apply(configuration, context); - } + MockMvcRestDocumentationConfigurer.this.apply(configuration, context); + MockMvcRestDocumentationConfigurer.this.uriConfigurer.apply(configuration, + context); return request; } } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 256d65271..25afe0ec2 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -16,14 +16,11 @@ package org.springframework.restdocs.restassured; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.config.AbstractConfigurer; import org.springframework.restdocs.config.RestDocumentationConfigurer; import com.jayway.restassured.filter.Filter; @@ -45,14 +42,10 @@ public final class RestAssuredRestDocumentationConfigurer private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer( this); - private final List configurers; - private final RestDocumentation restDocumentation; RestAssuredRestDocumentationConfigurer(RestDocumentation restDocumentation) { this.restDocumentation = restDocumentation; - this.configurers = Arrays.asList(snippets(), getTemplateEngineConfigurer(), - getWriterResolverConfigurer()); } @Override @@ -68,9 +61,7 @@ public Response filter(FilterableRequestSpecification requestSpec, Map configuration = new HashMap<>(); filterContext.setValue("org.springframework.restdocs.configuration", configuration); - for (AbstractConfigurer configurer : this.configurers) { - configurer.apply(configuration, context); - } + apply(configuration, context); return filterContext.next(requestSpec, responseSpec); } } From 27f919748bdf70b4bebb645a873cd3c5777fab89 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 3 Feb 2016 17:18:44 +0000 Subject: [PATCH 051/898] Upgrade to the latest version of the Spring IO Plugin --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index a286bdeed..75fd5ae99 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ buildscript { maven { url 'https://repo.spring.io/plugins-release' } } dependencies { - classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE' + classpath 'io.spring.gradle:dependency-management-plugin:0.5.5.RELEASE' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' - classpath 'io.spring.gradle:spring-io-plugin:0.0.4.RELEASE' + classpath 'io.spring.gradle:spring-io-plugin:0.0.5.RELEASE' } } From a9c9bad15ef03e4a0b72d6c11c6725a9cc140be5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 4 Feb 2016 11:33:40 +0000 Subject: [PATCH 052/898] Reduce duplication by introducing RestDocumentationHandler Previously, logic for creating an Operation, determining the snippets to call, and calling them was duplicated in both the MockMvc and REST Assured modules. This commit introduces a new core class, RestDocumentationHandler, that now does the bulk of the work in a reusable manner. The MockMvc and REST Assured modules have been updated to delegate to RestDocumentationHandler. Closes gh-194 --- .../restdocs/RestDocumentationException.java | 47 ++++ .../restdocs/RestDocumentationHandler.java | 232 ++++++++++++++++++ .../operation/ConversionException.java | 50 ++++ .../restdocs/operation/RequestConverter.java | 37 +++ .../restdocs/operation/ResponseConverter.java | 37 +++ .../RestDocumentationHandlerTests.java | 130 ++++++++++ ...tory.java => MockMvcRequestConverter.java} | 51 ++-- ...ory.java => MockMvcResponseConverter.java} | 17 +- .../mockmvc/MockMvcRestDocumentation.java | 23 +- .../MockMvcRestDocumentationConfigurer.java | 2 +- .../RestDocumentationResultHandler.java | 109 +------- ...java => MockMvcRequestConverterTests.java} | 21 +- ....java => RestAssuredRequestConverter.java} | 9 +- ...java => RestAssuredResponseConverter.java} | 8 +- .../RestAssuredRestDocumentation.java | 19 +- .../restassured/RestDocumentationFilter.java | 116 ++------- ... => RestAssuredRequestConverterTests.java} | 34 +-- 17 files changed, 662 insertions(+), 280 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java rename spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/{MockMvcOperationRequestFactory.java => MockMvcRequestConverter.java} (83%) rename spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/{MockMvcOperationResponseFactory.java => MockMvcResponseConverter.java} (77%) rename spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/{MockMvcOperationRequestFactoryTests.java => MockMvcRequestConverterTests.java} (92%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/{RestAssuredOperationRequestFactory.java => RestAssuredRequestConverter.java} (92%) rename spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/{RestAssuredOperationResponseFactory.java => RestAssuredResponseConverter.java} (84%) rename spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/{RestAssuredOperationRequestFactoryTests.java => RestAssuredRequestConverterTests.java} (85%) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java new file mode 100644 index 000000000..5bdf27c13 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2016 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; + +/** + * An exception that can be thrown when a failure occurs during REST documentation + * generation. + * + * @author Andy Wilkinson + */ +public class RestDocumentationException extends RuntimeException { + + /** + * Creates a new {@code RestDocumentationException} with the given {@code cause}. + * + * @param cause the cause + */ + public RestDocumentationException(Throwable cause) { + super(cause); + } + + /** + * Creates a new {@code RestDocumentationException} with the given {@code message} and + * {@code cause}. + * + * @param message the message + * @param cause the cause + */ + public RestDocumentationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java new file mode 100644 index 000000000..f8be483b8 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java @@ -0,0 +1,232 @@ +/* + * Copyright 2012-2016 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; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.restdocs.config.SnippetConfigurer; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.StandardOperation; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.Assert; + +/** + * A {@code RestDocumentationHandler} is used to produce documentation snippets from the + * request and response of an operation performed on a service. + * + * @param the request type that can be handled + * @param the response type that can be handled + * @author Andy Wilkinson + */ +public final class RestDocumentationHandler { + + private final String identifier; + + private final OperationRequestPreprocessor requestPreprocessor; + + private final OperationResponsePreprocessor responsePreprocessor; + + private final List snippets; + + private final RequestConverter requestConverter; + + private final ResponseConverter responseConverter; + + /** + * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * given {@code identifier}. The given {@code requestConverter} and + * {@code responseConverter} are used to convert the operation's request and response + * into generic {@code OperationRequest} and {@code OperationResponse} instances that + * can then be documented. The given documentation {@code snippets} will be produced. + * + * @param identifier the identifier for the operation + * @param requestConverter the request converter + * @param responseConverter the response converter + * @param snippets the snippets + */ + public RestDocumentationHandler(String identifier, + RequestConverter requestConverter, + ResponseConverter responseConverter, Snippet... snippets) { + this(identifier, requestConverter, responseConverter, + new IdentityOperationRequestPreprocessor(), + new IdentityOperationResponsePreprocessor(), snippets); + } + + /** + * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * given {@code identifier}. The given {@code requestConverter} and + * {@code responseConverter} are used to convert the operation's request and response + * into generic {@code OperationRequest} and {@code OperationResponse} instances that + * can then be documented. The given {@code requestPreprocessor} is applied to the + * request before it is documented. The given documentation {@code snippets} will be + * produced. + * + * @param identifier the identifier for the operation + * @param requestConverter the request converter + * @param responseConverter the response converter + * @param requestPreprocessor the request preprocessor + * @param snippets the snippets + */ + public RestDocumentationHandler(String identifier, + RequestConverter requestConverter, + ResponseConverter responseConverter, + OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { + this(identifier, requestConverter, responseConverter, requestPreprocessor, + new IdentityOperationResponsePreprocessor(), snippets); + } + + /** + * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * given {@code identifier}. The given {@code requestConverter} and + * {@code responseConverter} are used to convert the operation's request and response + * into generic {@code OperationRequest} and {@code OperationResponse} instances that + * can then be documented. The given {@code responsePreprocessor} is applied to the + * response before it is documented. The given documentation {@code snippets} will be + * produced. + * + * @param identifier the identifier for the operation + * @param requestConverter the request converter + * @param responseConverter the response converter + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + */ + public RestDocumentationHandler(String identifier, + RequestConverter requestConverter, + ResponseConverter responseConverter, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + this(identifier, requestConverter, responseConverter, + new IdentityOperationRequestPreprocessor(), responsePreprocessor, + snippets); + } + + /** + * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * given {@code identifier}. The given {@code requestConverter} and + * {@code responseConverter} are used to convert the operation's request and response + * into generic {@code OperationRequest} and {@code OperationResponse} instances that + * can then be documented. The given {@code requestPreprocessor} and + * {@code responsePreprocessor} are applied to the request and response before they + * are documented. The given documentation {@code snippets} will be produced. + * + * @param identifier the identifier for the operation + * @param requestConverter the request converter + * @param responseConverter the response converter + * @param requestPreprocessor the request preprocessor + * @param responsePreprocessor the response preprocessor + * @param snippets the snippets + */ + public RestDocumentationHandler(String identifier, + RequestConverter requestConverter, + ResponseConverter responseConverter, + OperationRequestPreprocessor requestPreprocessor, + OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { + Assert.notNull(identifier, "identifier must be non-null"); + Assert.notNull(requestConverter, "requestConverter must be non-null"); + Assert.notNull(responseConverter, "responseConverter must be non-null"); + Assert.notNull(identifier, "identifier must be non-null"); + Assert.notNull(requestPreprocessor, "requestPreprocessor must be non-null"); + Assert.notNull(responsePreprocessor, "responsePreprocessor must be non-null"); + Assert.notNull(snippets, "snippets must be non-null"); + this.identifier = identifier; + this.requestConverter = requestConverter; + this.responseConverter = responseConverter; + this.requestPreprocessor = requestPreprocessor; + this.responsePreprocessor = responsePreprocessor; + this.snippets = new ArrayList<>(Arrays.asList(snippets)); + } + + /** + * Handles the given {@code request} and {@code response}, producing documentation + * snippets for them using the given {@code configuration}. + * + * @param request the request + * @param response the request + * @param configuration the configuration + * @throws RestDocumentationException if a failure occurs during handling + */ + public void handle(REQ request, RESP response, Map configuration) { + OperationRequest operationRequest = this.requestPreprocessor + .preprocess(this.requestConverter.convert(request)); + + OperationResponse operationResponse = this.responsePreprocessor + .preprocess(this.responseConverter.convert(response)); + Map attributes = new HashMap<>(configuration); + Operation operation = new StandardOperation(this.identifier, operationRequest, + operationResponse, attributes); + try { + for (Snippet snippet : getSnippets(attributes)) { + snippet.document(operation); + } + } + catch (IOException ex) { + throw new RestDocumentationException(ex); + } + } + + /** + * Adds the given {@code snippets} such that they are documented when this handler is + * called. + * + * @param snippets the snippets to add + */ + public void addSnippets(Snippet... snippets) { + this.snippets.addAll(Arrays.asList(snippets)); + } + + @SuppressWarnings("unchecked") + private List getSnippets(Map configuration) { + List combinedSnippets = new ArrayList<>(this.snippets); + List defaultSnippets = (List) configuration + .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS); + if (defaultSnippets != null) { + combinedSnippets.addAll(defaultSnippets); + } + return combinedSnippets; + } + + private static final class IdentityOperationRequestPreprocessor implements + OperationRequestPreprocessor { + + @Override + public OperationRequest preprocess(OperationRequest request) { + return request; + } + + } + + private static final class IdentityOperationResponsePreprocessor implements + OperationResponsePreprocessor { + + @Override + public OperationResponse preprocess(OperationResponse response) { + return response; + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java new file mode 100644 index 000000000..397f79d9e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2016 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.operation; + +/** + * An exception that can be thrown by {@link RequestConverter} and + * {@link ResponseConverter} implementations to indicate that a failure has occurred + * during conversion. + * + * @author Andy Wilkinson + * @see RequestConverter#convert(Object) + * @see ResponseConverter#convert(Object) + */ +public class ConversionException extends RuntimeException { + + /** + * Creates a new {@code ConversionException} with the given {@code cause}. + * + * @param cause the cause + */ + public ConversionException(Throwable cause) { + super(cause); + } + + /** + * Creates a new {@code ConversionException} with the given {@code message} and + * {@code cause}. + * + * @param message the message + * @param cause the cause + */ + public ConversionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java new file mode 100644 index 000000000..5c2377d5a --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2016 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.operation; + +/** + * A {@code RequestConverter} is used to convert an implementation-specific request into + * an {@link OperationRequest}. + * + * @param The implementation-specific request type + * @author Andy Wilkinson + */ +public interface RequestConverter { + + /** + * Converts the given {@code request} into an {@code OperationRequest}. + * + * @param request the request + * @return the operation request + * @throws ConversionException if the conversion fails + */ + OperationRequest convert(R request); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java new file mode 100644 index 000000000..48a76be28 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2016 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.operation; + +/** + * A {@code ResponseConverter} is used to convert an implementation-specific response into + * an {@link OperationResponse}. + * + * @param The implementation-specific response type + * @author Andy Wilkinson + */ +public interface ResponseConverter { + + /** + * Converts the given {@code response} into an {@code OperationResponse}. + * + * @param response the response + * @return the operation response + * @throws ConversionException if the conversion fails + */ + OperationResponse convert(R response); + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java new file mode 100644 index 000000000..abfdafa55 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2016 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; + +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.config.SnippetConfigurer; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.snippet.Snippet; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link RestDocumentationHandler}. + * + * @author Andy Wilkinson + */ +public class RestDocumentationHandlerTests { + + @SuppressWarnings("unchecked") + private final RequestConverter requestConverter = mock(RequestConverter.class); + + @SuppressWarnings("unchecked") + private final ResponseConverter responseConverter = mock(ResponseConverter.class); + + private final Object request = new Object(); + + private final Object response = new Object(); + + private final OperationRequest operationRequest = new OperationRequestFactory() + .create(URI.create("http://localhost:8080"), null, null, new HttpHeaders(), + null, null); + + private final OperationResponse operationResponse = new OperationResponseFactory() + .create(null, null, null); + + private final Snippet snippet = mock(Snippet.class); + + @Test + public void basicHandling() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn( + this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn( + this.operationResponse); + HashMap configuration = new HashMap<>(); + new RestDocumentationHandler<>("id", this.requestConverter, + this.responseConverter, this.snippet).handle(this.request, this.response, + configuration); + verifySnippetInvocation(this.snippet, configuration); + } + + @Test + public void defaultSnippetsAreCalled() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn( + this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn( + this.operationResponse); + HashMap configuration = new HashMap<>(); + Snippet defaultSnippet1 = mock(Snippet.class); + Snippet defaultSnippet2 = mock(Snippet.class); + configuration.put(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS, + Arrays.asList(defaultSnippet1, defaultSnippet2)); + new RestDocumentationHandler<>("id", this.requestConverter, + this.responseConverter, this.snippet).handle(this.request, this.response, + configuration); + verifySnippetInvocation(this.snippet, configuration); + verifySnippetInvocation(defaultSnippet1, configuration); + verifySnippetInvocation(defaultSnippet2, configuration); + } + + @Test + public void additionalSnippetsAreCalled() throws IOException { + given(this.requestConverter.convert(this.request)).willReturn( + this.operationRequest); + given(this.responseConverter.convert(this.response)).willReturn( + this.operationResponse); + Snippet additionalSnippet1 = mock(Snippet.class); + Snippet additionalSnippet2 = mock(Snippet.class); + RestDocumentationHandler handler = new RestDocumentationHandler<>( + "id", this.requestConverter, this.responseConverter, this.snippet); + handler.addSnippets(additionalSnippet1, additionalSnippet2); + HashMap configuration = new HashMap<>(); + handler.handle(this.request, this.response, configuration); + verifySnippetInvocation(this.snippet, configuration); + verifySnippetInvocation(additionalSnippet1, configuration); + verifySnippetInvocation(additionalSnippet2, configuration); + } + + private void verifySnippetInvocation(Snippet snippet, Map attributes) + throws IOException { + ArgumentCaptor operation = ArgumentCaptor.forClass(Operation.class); + verify(snippet).document(operation.capture()); + assertThat(this.operationRequest, is(equalTo(operation.getValue().getRequest()))); + assertThat(this.operationResponse, + is(equalTo(operation.getValue().getResponse()))); + assertThat(attributes, is(equalTo(operation.getValue().getAttributes()))); + } +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java similarity index 83% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index 9449354a8..6ad0c5531 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -32,11 +32,13 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.restdocs.operation.ConversionException; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestFactory; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestConverter; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; @@ -44,13 +46,13 @@ import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; /** - * A factory for creating an {@link OperationRequest} from a + * A converter for creating an {@link OperationRequest} from a * {@link MockHttpServletRequest}. * * @author Andy Wilkinson * */ -class MockMvcOperationRequestFactory { +class MockMvcRequestConverter implements RequestConverter { private static final String SCHEME_HTTP = "http"; @@ -60,28 +62,29 @@ class MockMvcOperationRequestFactory { private static final int STANDARD_PORT_HTTPS = 443; - /** - * Creates a new {@code OperationRequest} derived from the given {@code mockRequest}. - * - * @param mockRequest the request - * @return the {@code OperationRequest} - * @throws Exception if the request could not be created - */ - OperationRequest createOperationRequest(MockHttpServletRequest mockRequest) - throws Exception { - HttpHeaders headers = extractHeaders(mockRequest); - Parameters parameters = extractParameters(mockRequest); - List parts = extractParts(mockRequest); - String queryString = mockRequest.getQueryString(); - if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { - queryString = parameters.toQueryString(); + @Override + public OperationRequest convert(MockHttpServletRequest mockRequest) { + try { + HttpHeaders headers = extractHeaders(mockRequest); + Parameters parameters = extractParameters(mockRequest); + List parts = extractParts(mockRequest); + String queryString = mockRequest.getQueryString(); + if (!StringUtils.hasText(queryString) + && "GET".equals(mockRequest.getMethod())) { + queryString = parameters.toQueryString(); + } + return new OperationRequestFactory() + .create(URI + .create(getRequestUri(mockRequest) + + (StringUtils.hasText(queryString) ? "?" + + queryString : "")), + HttpMethod.valueOf(mockRequest.getMethod()), FileCopyUtils + .copyToByteArray(mockRequest.getInputStream()), + headers, parameters, parts); + } + catch (Exception ex) { + throw new ConversionException(ex); } - return new OperationRequestFactory().create( - URI.create(getRequestUri(mockRequest) - + (StringUtils.hasText(queryString) ? "?" + queryString : "")), - HttpMethod.valueOf(mockRequest.getMethod()), - FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers, - parameters, parts); } private List extractParts(MockHttpServletRequest servletRequest) diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java similarity index 77% rename from spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java rename to spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java index 4e4f765f5..d0677cce9 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationResponseFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -21,22 +21,18 @@ import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.ResponseConverter; /** - * A factory for creating an {@link OperationResponse} derived from a + * A converter for creating an {@link OperationResponse} derived from a * {@link MockHttpServletResponse}. * * @author Andy Wilkinson */ -class MockMvcOperationResponseFactory { +class MockMvcResponseConverter implements ResponseConverter { - /** - * Create a new {@code OperationResponse} derived from the given {@code mockResponse}. - * - * @param mockResponse the response - * @return the {@code OperationResponse} - */ - OperationResponse createOperationResponse(MockHttpServletResponse mockResponse) { + @Override + public OperationResponse convert(MockHttpServletResponse mockResponse) { return new OperationResponseFactory().create( HttpStatus.valueOf(mockResponse.getStatus()), extractHeaders(mockResponse), mockResponse.getContentAsByteArray()); @@ -51,4 +47,5 @@ private HttpHeaders extractHeaders(MockHttpServletResponse response) { } return headers; } + } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index 541f380d9..64825f7ea 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.mockmvc; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationHandler; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; @@ -32,6 +33,10 @@ */ public abstract class MockMvcRestDocumentation { + private static final MockMvcRequestConverter REQUEST_CONVERTER = new MockMvcRequestConverter(); + + private static final MockMvcResponseConverter RESPONSE_CONVERTER = new MockMvcResponseConverter(); + private MockMvcRestDocumentation() { } @@ -61,7 +66,8 @@ public static MockMvcRestDocumentationConfigurer documentationConfiguration( */ public static RestDocumentationResultHandler document(String identifier, Snippet... snippets) { - return new RestDocumentationResultHandler(identifier, snippets); + return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } /** @@ -78,8 +84,9 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(identifier, requestPreprocessor, - snippets); + return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, + snippets)); } /** @@ -96,8 +103,9 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(identifier, responsePreprocessor, - snippets); + return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, responsePreprocessor, + snippets)); } /** @@ -117,8 +125,9 @@ public static RestDocumentationResultHandler document(String identifier, public static RestDocumentationResultHandler document(String identifier, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(identifier, requestPreprocessor, - responsePreprocessor, snippets); + return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, + responsePreprocessor, snippets)); } } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index a402487fe..7921cab8c 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -88,12 +88,12 @@ private ConfigurerApplyingRequestPostProcessor(RestDocumentation restDocumentati @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { RestDocumentationContext context = this.restDocumentation.beforeOperation(); - request.setAttribute(RestDocumentationContext.class.getName(), context); Map configuration = new HashMap<>(); configuration.put(MockHttpServletRequest.class.getName(), request); String urlTemplateAttribute = "org.springframework.restdocs.urlTemplate"; configuration.put(urlTemplateAttribute, request.getAttribute(urlTemplateAttribute)); + configuration.put(RestDocumentationContext.class.getName(), context); request.setAttribute("org.springframework.restdocs.configuration", configuration); MockMvcRestDocumentationConfigurer.this.apply(configuration, context); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 075c162fb..3194e826e 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -16,20 +16,11 @@ package org.springframework.restdocs.mockmvc; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; import java.util.Map; -import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.config.SnippetConfigurer; -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperation; -import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; -import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.RestDocumentationHandler; import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -44,68 +35,20 @@ */ public class RestDocumentationResultHandler implements ResultHandler { - private final String identifier; + private final RestDocumentationHandler delegate; - private final OperationRequestPreprocessor requestPreprocessor; - - private final OperationResponsePreprocessor responsePreprocessor; - - private final List snippets; - - RestDocumentationResultHandler(String identifier, Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - new IdentityOperationResponsePreprocessor(), snippets); - } - - RestDocumentationResultHandler(String identifier, - OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - this(identifier, requestPreprocessor, - new IdentityOperationResponsePreprocessor(), snippets); - } - - RestDocumentationResultHandler(String identifier, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - responsePreprocessor, snippets); - } - - RestDocumentationResultHandler(String identifier, - OperationRequestPreprocessor requestPreprocessor, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - Assert.notNull(identifier, "identifier must be non-null"); - Assert.notNull(requestPreprocessor, "requestPreprocessor must be non-null"); - Assert.notNull(responsePreprocessor, "responsePreprocessor must be non-null"); - Assert.notNull(snippets, "snippets must be non-null"); - this.identifier = identifier; - this.requestPreprocessor = requestPreprocessor; - this.responsePreprocessor = responsePreprocessor; - this.snippets = new ArrayList<>(Arrays.asList(snippets)); + RestDocumentationResultHandler( + RestDocumentationHandler delegate) { + Assert.notNull(delegate, "delegate must be non-null"); + this.delegate = delegate; } @Override public void handle(MvcResult result) throws Exception { - Map attributes = new HashMap<>(); - attributes.put(RestDocumentationContext.class.getName(), result.getRequest() - .getAttribute(RestDocumentationContext.class.getName())); - attributes.put("org.springframework.restdocs.urlTemplate", result.getRequest() - .getAttribute("org.springframework.restdocs.urlTemplate")); @SuppressWarnings("unchecked") Map configuration = (Map) result.getRequest() .getAttribute("org.springframework.restdocs.configuration"); - attributes.putAll(configuration); - - OperationRequest request = this.requestPreprocessor - .preprocess(new MockMvcOperationRequestFactory() - .createOperationRequest(result.getRequest())); - - OperationResponse response = this.responsePreprocessor - .preprocess(new MockMvcOperationResponseFactory() - .createOperationResponse(result.getResponse())); - Operation operation = new StandardOperation(this.identifier, request, response, - attributes); - for (Snippet snippet : getSnippets(result)) { - snippet.document(operation); - } + this.delegate.handle(result.getRequest(), result.getResponse(), configuration); } /** @@ -113,41 +56,11 @@ public void handle(MvcResult result) throws Exception { * handler is called. * * @param snippets the snippets to add - * @return this {@code ResultDocumentationResultHandler} + * @return this {@code RestDocumentationResultHandler} */ public RestDocumentationResultHandler snippets(Snippet... snippets) { - this.snippets.addAll(Arrays.asList(snippets)); + this.delegate.addSnippets(snippets); return this; } - @SuppressWarnings("unchecked") - private List getSnippets(MvcResult result) { - List combinedSnippets = new ArrayList<>( - (List) ((Map) result.getRequest().getAttribute( - "org.springframework.restdocs.configuration")) - .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS)); - combinedSnippets.addAll(this.snippets); - return combinedSnippets; - } - - private static final class IdentityOperationRequestPreprocessor implements - OperationRequestPreprocessor { - - @Override - public OperationRequest preprocess(OperationRequest request) { - return request; - } - - } - - private static final class IdentityOperationResponsePreprocessor implements - OperationResponsePreprocessor { - - @Override - public OperationResponse preprocess(OperationResponse response) { - return response; - } - - } - } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java similarity index 92% rename from spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java rename to spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index 6770d31bb..5c5c2aeaf 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -43,13 +43,13 @@ import static org.mockito.Mockito.mock; /** - * Tests for {@link MockMvcOperationRequestFactory}. + * Tests for {@link MockMvcRequestConverter}. * * @author Andy Wilkinson */ -public class MockMvcOperationRequestFactoryTests { +public class MockMvcRequestConverterTests { - private final MockMvcOperationRequestFactory factory = new MockMvcOperationRequestFactory(); + private final MockMvcRequestConverter factory = new MockMvcRequestConverter(); @Test public void httpRequest() throws Exception { @@ -64,7 +64,7 @@ public void httpRequestWithCustomPort() throws Exception { MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") .buildRequest(new MockServletContext()); mockRequest.setServerPort(8080); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getUri(), is(URI.create("http://localhost:8080/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -93,7 +93,7 @@ public void httpsRequest() throws Exception { .buildRequest(new MockServletContext()); mockRequest.setScheme("https"); mockRequest.setServerPort(443); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getUri(), is(URI.create("https://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -104,7 +104,7 @@ public void httpsRequestWithCustomPort() throws Exception { .buildRequest(new MockServletContext()); mockRequest.setScheme("https"); mockRequest.setServerPort(8443); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getUri(), is(URI.create("https://localhost:8443/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -191,7 +191,7 @@ public void requestWithPart() throws Exception { given(mockPart.getName()).willReturn("part-name"); given(mockPart.getSubmittedFileName()).willReturn("submitted.txt"); mockRequest.addPart(mockPart); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getParts().size(), is(1)); OperationRequestPart part = request.getParts().iterator().next(); assertThat(part.getName(), is(equalTo("part-name"))); @@ -216,7 +216,7 @@ public void requestWithPartWithContentType() throws Exception { given(mockPart.getSubmittedFileName()).willReturn("submitted.png"); given(mockPart.getContentType()).willReturn("image/png"); mockRequest.addPart(mockPart); - OperationRequest request = this.factory.createOperationRequest(mockRequest); + OperationRequest request = this.factory.convert(mockRequest); assertThat(request.getParts().size(), is(1)); OperationRequestPart part = request.getParts().iterator().next(); assertThat(part.getName(), is(equalTo("part-name"))); @@ -229,8 +229,7 @@ public void requestWithPartWithContentType() throws Exception { private OperationRequest createOperationRequest(MockHttpServletRequestBuilder builder) throws Exception { - return this.factory.createOperationRequest(builder - .buildRequest(new MockServletContext())); + return this.factory.convert(builder.buildRequest(new MockServletContext())); } } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java similarity index 92% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index c3ef0b20c..cfc19677d 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactory.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -30,20 +30,23 @@ import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestConverter; import com.jayway.restassured.response.Header; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.MultiPartSpecification; /** - * A factory for creating an {@link OperationRequest} derived from a REST Assured + * A converter for creating an {@link OperationRequest} from a REST Assured * {@link FilterableRequestSpecification}. * * @author Andy Wilkinson */ -class RestAssuredOperationRequestFactory { +class RestAssuredRequestConverter implements + RequestConverter { - OperationRequest createOperationRequest(FilterableRequestSpecification requestSpec) { + @Override + public OperationRequest convert(FilterableRequestSpecification requestSpec) { return new OperationRequestFactory().create(URI.create(requestSpec.getURI()), HttpMethod.valueOf(requestSpec.getMethod().name()), extractContent(requestSpec), extractHeaders(requestSpec), diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java similarity index 84% rename from spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java rename to spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java index bbcd251ee..425fc96fb 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredOperationResponseFactory.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredResponseConverter.java @@ -20,19 +20,21 @@ import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.ResponseConverter; import com.jayway.restassured.response.Header; import com.jayway.restassured.response.Response; /** - * A factory for creating an {@link OperationResponse} derived from a REST Assured + * A converter for creating an {@link OperationResponse} from a REST Assured * {@link Response}. * * @author Andy Wilkinson */ -class RestAssuredOperationResponseFactory { +class RestAssuredResponseConverter implements ResponseConverter { - OperationResponse createOperationResponse(Response response) { + @Override + public OperationResponse convert(Response response) { return new OperationResponseFactory().create( HttpStatus.valueOf(response.getStatusCode()), extractHeaders(response), extractContent(response)); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index 447ba1f55..0e47cbd24 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.restassured; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationHandler; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; @@ -28,6 +29,10 @@ */ public abstract class RestAssuredRestDocumentation { + private static final RestAssuredRequestConverter REQUEST_CONVERTER = new RestAssuredRequestConverter(); + + private static final RestAssuredResponseConverter RESPONSE_CONVERTER = new RestAssuredResponseConverter(); + private RestAssuredRestDocumentation() { } @@ -41,7 +46,8 @@ private RestAssuredRestDocumentation() { * @return a {@link RestDocumentationFilter} that will produce the documentation */ public static RestDocumentationFilter document(String identifier, Snippet... snippets) { - return new RestDocumentationFilter(identifier, snippets); + return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } /** @@ -56,7 +62,8 @@ public static RestDocumentationFilter document(String identifier, Snippet... sni */ public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(identifier, requestPreprocessor, snippets); + return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, snippets)); } /** @@ -71,7 +78,8 @@ public static RestDocumentationFilter document(String identifier, */ public static RestDocumentationFilter document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(identifier, responsePreprocessor, snippets); + return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + REQUEST_CONVERTER, RESPONSE_CONVERTER, responsePreprocessor, snippets)); } /** @@ -89,8 +97,9 @@ public static RestDocumentationFilter document(String identifier, public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(identifier, requestPreprocessor, - responsePreprocessor, snippets); + return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, + responsePreprocessor, snippets)); } /** diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 3460a99a3..f588ed168 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -16,22 +16,13 @@ package org.springframework.restdocs.restassured; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.config.SnippetConfigurer; -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.StandardOperation; -import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; -import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; +import org.springframework.restdocs.RestDocumentationHandler; import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.Assert; import com.jayway.restassured.filter.Filter; import com.jayway.restassured.filter.FilterContext; @@ -46,40 +37,12 @@ */ public final class RestDocumentationFilter implements Filter { - private final String identifier; + private final RestDocumentationHandler delegate; - private final OperationRequestPreprocessor requestPreprocessor; - - private final OperationResponsePreprocessor responsePreprocessor; - - private final List snippets; - - RestDocumentationFilter(String identifier, Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - new IdentityOperationResponsePreprocessor(), snippets); - } - - RestDocumentationFilter(String identifier, - OperationRequestPreprocessor operationRequestPreprocessor, - Snippet... snippets) { - this(identifier, operationRequestPreprocessor, - new IdentityOperationResponsePreprocessor(), snippets); - } - - RestDocumentationFilter(String identifier, - OperationResponsePreprocessor operationResponsePreprocessor, - Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - operationResponsePreprocessor, snippets); - } - - RestDocumentationFilter(String identifier, - OperationRequestPreprocessor requestPreprocessor, - OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - this.identifier = identifier; - this.requestPreprocessor = requestPreprocessor; - this.responsePreprocessor = responsePreprocessor; - this.snippets = new ArrayList<>(Arrays.asList(snippets)); + RestDocumentationFilter( + RestDocumentationHandler delegate) { + Assert.notNull(delegate, "delegate must be non-null"); + this.delegate = delegate; } @Override @@ -87,35 +50,15 @@ public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext context) { Response response = context.next(requestSpec, responseSpec); - OperationRequest operationRequest = this.requestPreprocessor - .preprocess(new RestAssuredOperationRequestFactory() - .createOperationRequest(requestSpec)); - OperationResponse operationResponse = this.responsePreprocessor - .preprocess(new RestAssuredOperationResponseFactory() - .createOperationResponse(response)); - - RestDocumentationContext documentationContext = context - .getValue(RestDocumentationContext.class.getName()); - - Map attributes = new HashMap<>(); - attributes.put(RestDocumentationContext.class.getName(), documentationContext); - attributes.put("org.springframework.restdocs.urlTemplate", + Map configuration = new HashMap<>( + context.>getValue("org.springframework.restdocs.configuration")); + configuration.put(RestDocumentationContext.class.getName(), context + .getValue(RestDocumentationContext.class + .getName())); + configuration.put("org.springframework.restdocs.urlTemplate", requestSpec.getUserDefinedPath()); - Map configuration = context - .getValue("org.springframework.restdocs.configuration"); - attributes.putAll(configuration); - Operation operation = new StandardOperation(this.identifier, operationRequest, - operationResponse, attributes); - - try { - for (Snippet snippet : getSnippets(configuration)) { - snippet.document(operation); - } - } - catch (IOException ex) { - throw new RuntimeException(ex); - } + this.delegate.handle(requestSpec, response, configuration); return response; } @@ -128,37 +71,8 @@ public Response filter(FilterableRequestSpecification requestSpec, * @return this {@code RestDocumentationFilter} */ public RestDocumentationFilter snippets(Snippet... snippets) { - this.snippets.addAll(Arrays.asList(snippets)); + this.delegate.addSnippets(snippets); return this; } - @SuppressWarnings("unchecked") - private List getSnippets(Map configuration) { - List combinedSnippets = new ArrayList<>( - (List) configuration - .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS)); - combinedSnippets.addAll(this.snippets); - return combinedSnippets; - } - - private static final class IdentityOperationRequestPreprocessor implements - OperationRequestPreprocessor { - - @Override - public OperationRequest preprocess(OperationRequest request) { - return request; - } - - } - - private static final class IdentityOperationResponsePreprocessor implements - OperationResponsePreprocessor { - - @Override - public OperationResponse preprocess(OperationResponse response) { - return response; - } - - } - } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java similarity index 85% rename from spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java rename to spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index bb5325598..f39b15b2b 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredOperationRequestFactoryTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -36,7 +36,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.restassured.RestAssuredOperationRequestFactoryTests.TestApplication; +import org.springframework.restdocs.restassured.RestAssuredRequestConverterTests.TestApplication; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.web.bind.annotation.RequestMapping; @@ -51,7 +51,7 @@ import static org.junit.Assert.assertThat; /** - * Tests for {@link RestAssuredOperationRequestFactory}. + * Tests for {@link RestAssuredRequestConverter}. * * @author Andy Wilkinson */ @@ -59,12 +59,12 @@ @SpringApplicationConfiguration(classes = TestApplication.class) @WebAppConfiguration @IntegrationTest("server.port=0") -public class RestAssuredOperationRequestFactoryTests { +public class RestAssuredRequestConverterTests { @Rule public final ExpectedException thrown = ExpectedException.none(); - private final RestAssuredOperationRequestFactory factory = new RestAssuredOperationRequestFactory(); + private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); @Value("${local.server.port}") private int port; @@ -74,7 +74,7 @@ public void requestUri() { RequestSpecification requestSpec = RestAssured.given().port(this.port); requestSpec.get("/foo/bar"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getUri(), is(equalTo(URI.create("http://localhost:" + this.port + "/foo/bar")))); } @@ -84,7 +84,7 @@ public void requestMethod() { RequestSpecification requestSpec = RestAssured.given().port(this.port); requestSpec.head("/foo/bar"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getMethod(), is(equalTo(HttpMethod.HEAD))); } @@ -94,7 +94,7 @@ public void queryStringParameters() { .queryParam("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getParameters().size(), is(1)); assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); } @@ -104,7 +104,7 @@ public void queryStringFromUrlParameters() { RequestSpecification requestSpec = RestAssured.given().port(this.port); requestSpec.get("/?foo=bar"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getParameters().size(), is(1)); assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); } @@ -115,7 +115,7 @@ public void formParameters() { .formParameter("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getParameters().size(), is(1)); assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); } @@ -126,7 +126,7 @@ public void requestParameters() { .parameter("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getParameters().size(), is(1)); assertThat(request.getParameters().get("foo"), is(equalTo(Arrays.asList("bar")))); } @@ -137,7 +137,7 @@ public void headers() { .header("Foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getHeaders().toString(), request.getHeaders().size(), is(3)); assertThat(request.getHeaders().get("Foo"), is(equalTo(Arrays.asList("bar")))); assertThat(request.getHeaders().get("Accept"), is(equalTo(Arrays.asList("*/*")))); @@ -152,7 +152,7 @@ public void multipart() { .multiPart("b", new ObjectBody("bar"), "application/json"); requestSpec.post().then().statusCode(200); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); Collection parts = request.getParts(); assertThat(parts.size(), is(2)); Iterator iterator = parts.iterator(); @@ -174,7 +174,7 @@ public void byteArrayBody() { RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()) .port(this.port); requestSpec.post(); - this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + this.factory.convert((FilterableRequestSpecification) requestSpec); } @Test @@ -183,7 +183,7 @@ public void stringBody() { .port(this.port); requestSpec.post(); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getContentAsString(), is(equalTo("body"))); } @@ -193,7 +193,7 @@ public void objectBody() { .body(new ObjectBody("bar")).port(this.port); requestSpec.post(); OperationRequest request = this.factory - .createOperationRequest((FilterableRequestSpecification) requestSpec); + .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getContentAsString(), is(equalTo("{\"foo\":\"bar\"}"))); } @@ -205,7 +205,7 @@ public void inputStreamBodyIsNotSupported() { requestSpec.post(); this.thrown .expectMessage(equalTo("Unsupported request content: java.io.ByteArrayInputStream")); - this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + this.factory.convert((FilterableRequestSpecification) requestSpec); } @Test @@ -214,7 +214,7 @@ public void fileBodyIsNotSupported() { .body(new File("src/test/resources/body.txt")).port(this.port); requestSpec.post(); this.thrown.expectMessage(equalTo("Unsupported request content: java.io.File")); - this.factory.createOperationRequest((FilterableRequestSpecification) requestSpec); + this.factory.convert((FilterableRequestSpecification) requestSpec); } /** From 1897ec54f202f35d0aab2b0b1a39b734374dd4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Thu, 4 Feb 2016 11:55:03 +0100 Subject: [PATCH 053/898] Add support for removing headers that match a Pattern A new preprocessor method `removeMatchingHeaders` has been introduced which takes one or more regular expression patterns (as Strings). Any header that matches a pattern is removed. Closes gh-195 --- .../customizing-requests-and-responses.adoc | 2 + .../HeaderRemovingOperationPreprocessor.java | 40 +++++++++++++++---- .../operation/preprocess/Preprocessors.java | 17 +++++++- ...derRemovingOperationPreprocessorTests.java | 32 +++++++++++++-- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 900106388..6011df8d7 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -105,6 +105,8 @@ different replacement can also be specified if you wish. `removeHeaders` on `Preprocessors` removes any occurrences of the named headers from the request or response. +`removeMatchingHeaders` on `Preprocessors` applies the given patterns on every header and +removes them when matching. [[customizing-requests-and-responses-preprocessors-replace-patterns]] diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java index 7766af57c..689acc8ff 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.regex.Pattern; import org.springframework.http.HttpHeaders; import org.springframework.restdocs.operation.OperationRequest; @@ -27,20 +28,28 @@ import org.springframework.restdocs.operation.OperationResponseFactory; /** - * An {@link OperationPreprocessor} that removes headers. + * An {@link OperationPreprocessor} that removes headers. The headers to remove are + * provided as constructor arguments and can be either plain string or patterns to match + * against the headers found * * @author Andy Wilkinson */ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - private final Set headersToRemove; + private final Set plainHeadersToRemove; + private final Set patternHeadersToRemove; + + HeaderRemovingOperationPreprocessor(String ... headersToRemove) { + this.plainHeadersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); + this.patternHeadersToRemove = null; + } - HeaderRemovingOperationPreprocessor(String... headersToRemove) { - this.headersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); + HeaderRemovingOperationPreprocessor(Pattern ... patternHeadersToRemove) { + this.plainHeadersToRemove = null; + this.patternHeadersToRemove = new HashSet<>(Arrays.asList(patternHeadersToRemove)); } @Override @@ -58,8 +67,25 @@ public OperationRequest preprocess(OperationRequest request) { private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { HttpHeaders processedHeaders = new HttpHeaders(); processedHeaders.putAll(originalHeaders); - for (String headerToRemove : this.headersToRemove) { - processedHeaders.remove(headerToRemove); + if (this.plainHeadersToRemove != null) { + for (String headerToRemove : this.plainHeadersToRemove) { + processedHeaders.remove(headerToRemove); + } + } + else { + Set toRemove = new HashSet<>(); + for (String headerToCheck : originalHeaders.keySet()) { + for (Pattern pattern : this.patternHeadersToRemove) { + if (pattern.matcher(headerToCheck).matches()) { + toRemove.add(headerToCheck); + } + } + } + // Remove afterwards to avoid side effects when removing while iterating over + // the set keys : + for (String headerToRemove : toRemove) { + processedHeaders.remove(headerToRemove); + } } return processedHeaders; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index b4aa590dd..bacb4fd73 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -76,13 +76,28 @@ public static OperationPreprocessor prettyPrint() { * Returns an {@code OperationPreprocessor} that will remove headers from the request * or response. * - * @param headersToRemove the names of the headers to remove + * @param headersToRemove the names of the headers to remove. * @return the preprocessor */ public static OperationPreprocessor removeHeaders(String... headersToRemove) { return new HeaderRemovingOperationPreprocessor(headersToRemove); } + /** + * Returns an {@code OperationPreprocessor} that will remove headers from the request + * or response based on a pattern match. + * + * @param headerPatternsToRemove pattern for the header names to remove. Every matchig header will be removed. + * @return the preprocessor + */ + public static OperationPreprocessor removeMatchingHeaders(String... headerPatternsToRemove) { + Pattern[] patterns = new Pattern[headerPatternsToRemove.length]; + for (int i = 0; i < headerPatternsToRemove.length; i++) { + patterns[i] = Pattern.compile(headerPatternsToRemove[i]); + } + return new HeaderRemovingOperationPreprocessor(patterns); + } + /** * Returns an {@code OperationPreprocessor} that will mask the href of hypermedia * links in the request or response. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index 634ef6162..99a661354 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -19,6 +19,7 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; +import java.util.regex.Pattern; import org.junit.Test; import org.springframework.http.HttpHeaders; @@ -66,18 +67,43 @@ public void modifyRequestHeaders() { @Test public void modifyResponseHeaders() { - OperationResponse response = this.responseFactory.create(HttpStatus.OK, - getHttpHeaders(), new byte[0]); + OperationResponse response = createResponse(); OperationResponse preprocessed = this.preprocessor.preprocess(response); assertThat(preprocessed.getHeaders().size(), is(equalTo(1))); assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); } - private HttpHeaders getHttpHeaders() { + @Test + public void modifyWithPattern() { + OperationResponse response = createResponse("content-length", "1234"); + HeaderRemovingOperationPreprocessor processor = + new HeaderRemovingOperationPreprocessor(Pattern.compile("co.*le(.)gth]")); + OperationResponse preprocessed = processor.preprocess(response); + assertThat(preprocessed.getHeaders().size(), is(equalTo(2))); + assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); + assertThat(preprocessed.getHeaders(), hasEntry("b", Arrays.asList("bravo", "banana"))); + } + + @Test + public void removeAllHeaders() { + HeaderRemovingOperationPreprocessor processor = + new HeaderRemovingOperationPreprocessor(Pattern.compile(".*")); + OperationResponse preprocessed = processor.preprocess(createResponse()); + assertThat(preprocessed.getHeaders().size(), is(equalTo(0))); + } + + private OperationResponse createResponse(String ... extraHeaders) { + return this.responseFactory.create(HttpStatus.OK, getHttpHeaders(extraHeaders), new byte[0]); + } + + private HttpHeaders getHttpHeaders(String ... extraHeaders) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("a", "alpha"); httpHeaders.add("b", "bravo"); httpHeaders.add("b", "banana"); + for (int i = 0; i < extraHeaders.length; i += 2) { + httpHeaders.add(extraHeaders[i], extraHeaders[i + 1]); + } return httpHeaders; } From 1131c9fa6ee7137f6b85dbbf183b39eb3321dec8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Feb 2016 09:32:14 +0000 Subject: [PATCH 054/898] Polish pattern-based header removal contribution Closes gh-191 --- .../customizing-requests-and-responses.adoc | 8 +-- .../preprocess/ExactMatchHeaderFilter.java | 41 +++++++++++++++ .../operation/preprocess/HeaderFilter.java | 35 +++++++++++++ .../HeaderRemovingOperationPreprocessor.java | 43 ++++------------ .../preprocess/PatternMatchHeaderFilter.java | 50 +++++++++++++++++++ .../operation/preprocess/Preprocessors.java | 32 ++++++------ ...derRemovingOperationPreprocessorTests.java | 25 +++++----- 7 files changed, 171 insertions(+), 63 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ExactMatchHeaderFilter.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternMatchHeaderFilter.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 6011df8d7..8cb020544 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -102,11 +102,11 @@ different replacement can also be specified if you wish. [[customizing-requests-and-responses-preprocessors-remove-headers]] ==== Removing headers -`removeHeaders` on `Preprocessors` removes any occurrences of the named headers -from the request or response. +`removeHeaders` on `Preprocessors` removes any headers from the request or response where +the name is equal to any of the given header names. -`removeMatchingHeaders` on `Preprocessors` applies the given patterns on every header and -removes them when matching. +`removeMatchingHeaders` on `Preprocessors` removes any headers from the request or +response where the name matches any of the given regular expression patterns. [[customizing-requests-and-responses-preprocessors-replace-patterns]] diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ExactMatchHeaderFilter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ExactMatchHeaderFilter.java new file mode 100644 index 000000000..6e1cd8731 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ExactMatchHeaderFilter.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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.operation.preprocess; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * A {@link HeaderFilter} that excludes a header if its name is an exact match. + * + * @author Andy Wilkinson + */ +class ExactMatchHeaderFilter implements HeaderFilter { + + private final Set headersToExclude; + + ExactMatchHeaderFilter(String... headersToExclude) { + this.headersToExclude = new HashSet<>(Arrays.asList(headersToExclude)); + } + + @Override + public boolean excludeHeader(String name) { + return this.headersToExclude.contains(name); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java new file mode 100644 index 000000000..6b14f2494 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderFilter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2014-2016 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.operation.preprocess; + +/** + * A strategy for determining whether or not a header should be excluded. + * + * @author Andy Wilkinson + */ +interface HeaderFilter { + + /** + * Called to determine whether a header should be excluded. Return {@code true} to + * exclude a header, otherwise {@code false}. + * + * @param name the name of the header + * @return {@code true} to exclude the header, otherwise {@code false} + */ + boolean excludeHeader(String name); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java index 689acc8ff..749f61055 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,10 +16,7 @@ package org.springframework.restdocs.operation.preprocess; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.regex.Pattern; +import java.util.Iterator; import org.springframework.http.HttpHeaders; import org.springframework.restdocs.operation.OperationRequest; @@ -37,19 +34,13 @@ class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - private final Set plainHeadersToRemove; - private final Set patternHeadersToRemove; + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - HeaderRemovingOperationPreprocessor(String ... headersToRemove) { - this.plainHeadersToRemove = new HashSet<>(Arrays.asList(headersToRemove)); - this.patternHeadersToRemove = null; - } + private final HeaderFilter headerFilter; - HeaderRemovingOperationPreprocessor(Pattern ... patternHeadersToRemove) { - this.plainHeadersToRemove = null; - this.patternHeadersToRemove = new HashSet<>(Arrays.asList(patternHeadersToRemove)); + HeaderRemovingOperationPreprocessor(HeaderFilter headerFilter) { + this.headerFilter = headerFilter; } @Override @@ -67,24 +58,10 @@ public OperationRequest preprocess(OperationRequest request) { private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { HttpHeaders processedHeaders = new HttpHeaders(); processedHeaders.putAll(originalHeaders); - if (this.plainHeadersToRemove != null) { - for (String headerToRemove : this.plainHeadersToRemove) { - processedHeaders.remove(headerToRemove); - } - } - else { - Set toRemove = new HashSet<>(); - for (String headerToCheck : originalHeaders.keySet()) { - for (Pattern pattern : this.patternHeadersToRemove) { - if (pattern.matcher(headerToCheck).matches()) { - toRemove.add(headerToCheck); - } - } - } - // Remove afterwards to avoid side effects when removing while iterating over - // the set keys : - for (String headerToRemove : toRemove) { - processedHeaders.remove(headerToRemove); + Iterator headers = processedHeaders.keySet().iterator(); + while (headers.hasNext()) { + if (this.headerFilter.excludeHeader(headers.next())) { + headers.remove(); } } return processedHeaders; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternMatchHeaderFilter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternMatchHeaderFilter.java new file mode 100644 index 000000000..6fff33865 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternMatchHeaderFilter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014-2016 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.operation.preprocess; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A {@link HeaderFilter} that excludes a header if its name matches a {@link Pattern}. + * + * @author Andy Wilkinson + * @author Roland Huss + */ +class PatternMatchHeaderFilter implements HeaderFilter { + + private Set exclusionPatterns; + + PatternMatchHeaderFilter(String... exclusionPatterns) { + this.exclusionPatterns = new HashSet<>(); + for (String exclusionPattern : exclusionPatterns) { + this.exclusionPatterns.add(Pattern.compile(exclusionPattern)); + } + } + + @Override + public boolean excludeHeader(String name) { + for (Pattern pattern : this.exclusionPatterns) { + if (pattern.matcher(name).matches()) { + return true; + } + } + return false; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index bacb4fd73..a0bae926d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -30,6 +30,7 @@ * documented. * * @author Andy Wilkinson + * @author Roland Huss */ public final class Preprocessors { @@ -73,29 +74,32 @@ public static OperationPreprocessor prettyPrint() { } /** - * Returns an {@code OperationPreprocessor} that will remove headers from the request - * or response. + * Returns an {@code OperationPreprocessor} that will remove any header from the + * request or response with a name that is equal to one of the given + * {@code headersToRemove}. * - * @param headersToRemove the names of the headers to remove. + * @param headerNames the header names * @return the preprocessor + * @see String#equals(Object) */ - public static OperationPreprocessor removeHeaders(String... headersToRemove) { - return new HeaderRemovingOperationPreprocessor(headersToRemove); + public static OperationPreprocessor removeHeaders(String... headerNames) { + return new HeaderRemovingOperationPreprocessor(new ExactMatchHeaderFilter( + headerNames)); } /** - * Returns an {@code OperationPreprocessor} that will remove headers from the request - * or response based on a pattern match. + * Returns an {@code OperationPreprocessor} that will remove any headers from the + * request or response with a name that matches one of the given + * {@code headerNamePatterns} regular expressions. * - * @param headerPatternsToRemove pattern for the header names to remove. Every matchig header will be removed. + * @param headerNamePatterns the header name patterns * @return the preprocessor + * @see java.util.regex.Matcher#matches() */ - public static OperationPreprocessor removeMatchingHeaders(String... headerPatternsToRemove) { - Pattern[] patterns = new Pattern[headerPatternsToRemove.length]; - for (int i = 0; i < headerPatternsToRemove.length; i++) { - patterns[i] = Pattern.compile(headerPatternsToRemove[i]); - } - return new HeaderRemovingOperationPreprocessor(patterns); + public static OperationPreprocessor removeMatchingHeaders( + String... headerNamePatterns) { + return new HeaderRemovingOperationPreprocessor(new PatternMatchHeaderFilter( + headerNamePatterns)); } /** diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index 99a661354..a0e5c3804 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,7 +19,6 @@ import java.net.URI; import java.util.Arrays; import java.util.Collections; -import java.util.regex.Pattern; import org.junit.Test; import org.springframework.http.HttpHeaders; @@ -41,7 +40,7 @@ * Tests for {@link HeaderRemovingOperationPreprocessorTests}. * * @author Andy Wilkinson - * + * @author Roland Huss */ public class HeaderRemovingOperationPreprocessorTests { @@ -50,7 +49,7 @@ public class HeaderRemovingOperationPreprocessorTests { private final OperationResponseFactory responseFactory = new OperationResponseFactory(); private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor( - "b"); + new ExactMatchHeaderFilter("b")); @Test public void modifyRequestHeaders() { @@ -76,27 +75,29 @@ public void modifyResponseHeaders() { @Test public void modifyWithPattern() { OperationResponse response = createResponse("content-length", "1234"); - HeaderRemovingOperationPreprocessor processor = - new HeaderRemovingOperationPreprocessor(Pattern.compile("co.*le(.)gth]")); + HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( + new PatternMatchHeaderFilter("co.*le(.)gth]")); OperationResponse preprocessed = processor.preprocess(response); assertThat(preprocessed.getHeaders().size(), is(equalTo(2))); assertThat(preprocessed.getHeaders(), hasEntry("a", Arrays.asList("alpha"))); - assertThat(preprocessed.getHeaders(), hasEntry("b", Arrays.asList("bravo", "banana"))); + assertThat(preprocessed.getHeaders(), + hasEntry("b", Arrays.asList("bravo", "banana"))); } @Test public void removeAllHeaders() { - HeaderRemovingOperationPreprocessor processor = - new HeaderRemovingOperationPreprocessor(Pattern.compile(".*")); + HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( + new PatternMatchHeaderFilter(".*")); OperationResponse preprocessed = processor.preprocess(createResponse()); assertThat(preprocessed.getHeaders().size(), is(equalTo(0))); } - private OperationResponse createResponse(String ... extraHeaders) { - return this.responseFactory.create(HttpStatus.OK, getHttpHeaders(extraHeaders), new byte[0]); + private OperationResponse createResponse(String... extraHeaders) { + return this.responseFactory.create(HttpStatus.OK, getHttpHeaders(extraHeaders), + new byte[0]); } - private HttpHeaders getHttpHeaders(String ... extraHeaders) { + private HttpHeaders getHttpHeaders(String... extraHeaders) { HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add("a", "alpha"); httpHeaders.add("b", "bravo"); From c0fac14fdf1f7628e83b24e93023357768009671 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Feb 2016 21:15:33 +0000 Subject: [PATCH 055/898] Break cycle caused by introduction of RestDocumentationHandler --- ...RestDocumentationGenerationException.java} | 8 +++--- .../RestDocumentationGenerator.java} | 28 +++++++++---------- .../restdocs/generate/package-info.java | 21 ++++++++++++++ ...a => RestDocumentationGeneratorTests.java} | 15 +++++----- .../mockmvc/MockMvcRestDocumentation.java | 10 +++---- .../RestDocumentationResultHandler.java | 6 ++-- .../RestAssuredRestDocumentation.java | 10 +++---- .../restassured/RestDocumentationFilter.java | 6 ++-- 8 files changed, 63 insertions(+), 41 deletions(-) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{RestDocumentationException.java => generate/RestDocumentationGenerationException.java} (80%) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{RestDocumentationHandler.java => generate/RestDocumentationGenerator.java} (89%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{RestDocumentationHandlerTests.java => RestDocumentationGeneratorTests.java} (90%) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java similarity index 80% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java index 5bdf27c13..e1e76d8c2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.generate; /** * An exception that can be thrown when a failure occurs during REST documentation @@ -22,14 +22,14 @@ * * @author Andy Wilkinson */ -public class RestDocumentationException extends RuntimeException { +public class RestDocumentationGenerationException extends RuntimeException { /** * Creates a new {@code RestDocumentationException} with the given {@code cause}. * * @param cause the cause */ - public RestDocumentationException(Throwable cause) { + public RestDocumentationGenerationException(Throwable cause) { super(cause); } @@ -40,7 +40,7 @@ public RestDocumentationException(Throwable cause) { * @param message the message * @param cause the cause */ - public RestDocumentationException(String message, Throwable cause) { + public RestDocumentationGenerationException(String message, Throwable cause) { super(message, cause); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java similarity index 89% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java index f8be483b8..b60f5d9fd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs; +package org.springframework.restdocs.generate; import java.io.IOException; import java.util.ArrayList; @@ -36,14 +36,14 @@ import org.springframework.util.Assert; /** - * A {@code RestDocumentationHandler} is used to produce documentation snippets from the - * request and response of an operation performed on a service. + * A {@code RestDocumentationGenerator} is used to generate documentation snippets from + * the request and response of an operation performed on a service. * * @param the request type that can be handled * @param the response type that can be handled * @author Andy Wilkinson */ -public final class RestDocumentationHandler { +public final class RestDocumentationGenerator { private final String identifier; @@ -58,7 +58,7 @@ public final class RestDocumentationHandler { private final ResponseConverter responseConverter; /** - * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * Creates a new {@code RestDocumentationGenerator} for the operation identified by the * given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that @@ -69,7 +69,7 @@ public final class RestDocumentationHandler { * @param responseConverter the response converter * @param snippets the snippets */ - public RestDocumentationHandler(String identifier, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, ResponseConverter responseConverter, Snippet... snippets) { this(identifier, requestConverter, responseConverter, @@ -78,7 +78,7 @@ public RestDocumentationHandler(String identifier, } /** - * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * Creates a new {@code RestDocumentationGenerator} for the operation identified by the * given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that @@ -92,7 +92,7 @@ public RestDocumentationHandler(String identifier, * @param requestPreprocessor the request preprocessor * @param snippets the snippets */ - public RestDocumentationHandler(String identifier, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, ResponseConverter responseConverter, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { @@ -101,7 +101,7 @@ public RestDocumentationHandler(String identifier, } /** - * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * Creates a new {@code RestDocumentationGenerator} for the operation identified by the * given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that @@ -115,7 +115,7 @@ public RestDocumentationHandler(String identifier, * @param responsePreprocessor the response preprocessor * @param snippets the snippets */ - public RestDocumentationHandler(String identifier, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, ResponseConverter responseConverter, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { @@ -125,7 +125,7 @@ public RestDocumentationHandler(String identifier, } /** - * Creates a new {@code RestDocumentationHandler} for the operation identified by the + * Creates a new {@code RestDocumentationGenerator} for the operation identified by the * given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that @@ -140,7 +140,7 @@ public RestDocumentationHandler(String identifier, * @param responsePreprocessor the response preprocessor * @param snippets the snippets */ - public RestDocumentationHandler(String identifier, + public RestDocumentationGenerator(String identifier, RequestConverter requestConverter, ResponseConverter responseConverter, OperationRequestPreprocessor requestPreprocessor, @@ -167,7 +167,7 @@ public RestDocumentationHandler(String identifier, * @param request the request * @param response the request * @param configuration the configuration - * @throws RestDocumentationException if a failure occurs during handling + * @throws RestDocumentationGenerationException if a failure occurs during handling */ public void handle(REQ request, RESP response, Map configuration) { OperationRequest operationRequest = this.requestPreprocessor @@ -184,7 +184,7 @@ public void handle(REQ request, RESP response, Map configuration } } catch (IOException ex) { - throw new RestDocumentationException(ex); + throw new RestDocumentationGenerationException(ex); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java new file mode 100644 index 000000000..6b6ba8636 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Classes that drive the generation of the documentaiton snippets. + */ +package org.springframework.restdocs.generate; + diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java similarity index 90% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index abfdafa55..d03aa6dae 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationHandlerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -26,6 +26,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.http.HttpHeaders; import org.springframework.restdocs.config.SnippetConfigurer; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestFactory; @@ -43,11 +44,11 @@ import static org.mockito.Mockito.verify; /** - * Tests for {@link RestDocumentationHandler}. + * Tests for {@link RestDocumentationGenerator}. * * @author Andy Wilkinson */ -public class RestDocumentationHandlerTests { +public class RestDocumentationGeneratorTests { @SuppressWarnings("unchecked") private final RequestConverter requestConverter = mock(RequestConverter.class); @@ -75,7 +76,7 @@ public void basicHandling() throws IOException { given(this.responseConverter.convert(this.response)).willReturn( this.operationResponse); HashMap configuration = new HashMap<>(); - new RestDocumentationHandler<>("id", this.requestConverter, + new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, this.snippet).handle(this.request, this.response, configuration); verifySnippetInvocation(this.snippet, configuration); @@ -92,7 +93,7 @@ public void defaultSnippetsAreCalled() throws IOException { Snippet defaultSnippet2 = mock(Snippet.class); configuration.put(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS, Arrays.asList(defaultSnippet1, defaultSnippet2)); - new RestDocumentationHandler<>("id", this.requestConverter, + new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, this.snippet).handle(this.request, this.response, configuration); verifySnippetInvocation(this.snippet, configuration); @@ -108,11 +109,11 @@ public void additionalSnippetsAreCalled() throws IOException { this.operationResponse); Snippet additionalSnippet1 = mock(Snippet.class); Snippet additionalSnippet2 = mock(Snippet.class); - RestDocumentationHandler handler = new RestDocumentationHandler<>( + RestDocumentationGenerator generator = new RestDocumentationGenerator<>( "id", this.requestConverter, this.responseConverter, this.snippet); - handler.addSnippets(additionalSnippet1, additionalSnippet2); + generator.addSnippets(additionalSnippet1, additionalSnippet2); HashMap configuration = new HashMap<>(); - handler.handle(this.request, this.response, configuration); + generator.handle(this.request, this.response, configuration); verifySnippetInvocation(this.snippet, configuration); verifySnippetInvocation(additionalSnippet1, configuration); verifySnippetInvocation(additionalSnippet2, configuration); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index 64825f7ea..cf1840cbe 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -17,7 +17,7 @@ package org.springframework.restdocs.mockmvc; import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.RestDocumentationHandler; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; @@ -66,7 +66,7 @@ public static MockMvcRestDocumentationConfigurer documentationConfiguration( */ public static RestDocumentationResultHandler document(String identifier, Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } @@ -84,7 +84,7 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, snippets)); } @@ -103,7 +103,7 @@ public static RestDocumentationResultHandler document(String identifier, */ public static RestDocumentationResultHandler document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, responsePreprocessor, snippets)); } @@ -125,7 +125,7 @@ public static RestDocumentationResultHandler document(String identifier, public static RestDocumentationResultHandler document(String identifier, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationResultHandler(new RestDocumentationHandler<>( + return new RestDocumentationResultHandler(new RestDocumentationGenerator<>( identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 3194e826e..d536da132 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -20,7 +20,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.restdocs.RestDocumentationHandler; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; @@ -35,10 +35,10 @@ */ public class RestDocumentationResultHandler implements ResultHandler { - private final RestDocumentationHandler delegate; + private final RestDocumentationGenerator delegate; RestDocumentationResultHandler( - RestDocumentationHandler delegate) { + RestDocumentationGenerator delegate) { Assert.notNull(delegate, "delegate must be non-null"); this.delegate = delegate; } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index 0e47cbd24..6f2c3f4ce 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -17,7 +17,7 @@ package org.springframework.restdocs.restassured; import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.RestDocumentationHandler; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; @@ -46,7 +46,7 @@ private RestAssuredRestDocumentation() { * @return a {@link RestDocumentationFilter} that will produce the documentation */ public static RestDocumentationFilter document(String identifier, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets)); } @@ -62,7 +62,7 @@ public static RestDocumentationFilter document(String identifier, Snippet... sni */ public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, snippets)); } @@ -78,7 +78,7 @@ public static RestDocumentationFilter document(String identifier, */ public static RestDocumentationFilter document(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, responsePreprocessor, snippets)); } @@ -97,7 +97,7 @@ public static RestDocumentationFilter document(String identifier, public static RestDocumentationFilter document(String identifier, OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - return new RestDocumentationFilter(new RestDocumentationHandler<>(identifier, + return new RestDocumentationFilter(new RestDocumentationGenerator<>(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, requestPreprocessor, responsePreprocessor, snippets)); } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index f588ed168..29a0b2571 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -20,7 +20,7 @@ import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.RestDocumentationHandler; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.Snippet; import org.springframework.util.Assert; @@ -37,10 +37,10 @@ */ public final class RestDocumentationFilter implements Filter { - private final RestDocumentationHandler delegate; + private final RestDocumentationGenerator delegate; RestDocumentationFilter( - RestDocumentationHandler delegate) { + RestDocumentationGenerator delegate) { Assert.notNull(delegate, "delegate must be non-null"); this.delegate = delegate; } From cb9e10bf5af775c7f106138171fd8c274be10037 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Feb 2016 10:56:12 +0000 Subject: [PATCH 056/898] Break the package cycle caused by the introduction of SnippetFormat --- config/checkstyle/checkstyle.xml | 2 +- docs/src/docs/asciidoc/configuration.adoc | 8 +- .../com/example/mockmvc/CustomFormat.java | 4 +- .../com/example/restassured/CustomFormat.java | 4 +- .../config/RestDocumentationConfigurer.java | 4 +- .../restdocs/config/SnippetConfiguration.java | 10 +- .../restdocs/config/SnippetConfigurer.java | 20 ++-- .../restdocs/snippet/SnippetFormats.java | 74 --------------- .../snippet/StandardWriterResolver.java | 20 ++-- .../StandardTemplateResourceResolver.java | 19 ++-- .../TemplateFormat.java} | 18 +++- .../restdocs/templates/TemplateFormats.java | 93 +++++++++++++++++++ .../curl-request.snippet | 0 .../http-request.snippet | 0 .../http-response.snippet | 0 .../{adoc => asciidoctor}/links.snippet | 0 .../path-parameters.snippet | 0 .../request-fields.snippet | 0 .../request-headers.snippet | 0 .../request-parameters.snippet | 0 .../response-fields.snippet | 0 .../response-headers.snippet | 0 .../{md => markdown}/curl-request.snippet | 0 .../{md => markdown}/http-request.snippet | 0 .../{md => markdown}/http-response.snippet | 0 .../templates/{md => markdown}/links.snippet | 0 .../{md => markdown}/path-parameters.snippet | 0 .../{md => markdown}/request-fields.snippet | 0 .../{md => markdown}/request-headers.snippet | 0 .../request-parameters.snippet | 0 .../{md => markdown}/response-fields.snippet | 0 .../{md => markdown}/response-headers.snippet | 0 .../restdocs/AbstractSnippetTests.java | 30 +++--- .../RestDocumentationConfigurerTests.java | 14 +-- .../curl/CurlRequestSnippetTests.java | 6 +- .../RequestHeadersSnippetFailureTests.java | 4 +- .../headers/RequestHeadersSnippetTests.java | 6 +- .../ResponseHeadersSnippetFailureTests.java | 4 +- .../headers/ResponseHeadersSnippetTests.java | 6 +- .../http/HttpRequestSnippetTests.java | 6 +- .../http/HttpResponseSnippetTests.java | 6 +- .../hypermedia/LinksSnippetFailureTests.java | 4 +- .../hypermedia/LinksSnippetTests.java | 6 +- .../AsciidoctorRequestFieldsSnippetTests.java | 11 ++- .../RequestFieldsSnippetFailureTests.java | 4 +- .../payload/RequestFieldsSnippetTests.java | 6 +- .../ResponseFieldsSnippetFailureTests.java | 4 +- .../payload/ResponseFieldsSnippetTests.java | 6 +- .../PathParametersSnippetFailureTests.java | 4 +- .../request/PathParametersSnippetTests.java | 6 +- .../RequestParametersSnippetFailureTests.java | 4 +- .../RequestParametersSnippetTests.java | 6 +- .../snippet/StandardWriterResolverTests.java | 2 +- ...StandardTemplateResourceResolverTests.java | 6 +- .../restdocs/test/ExpectedSnippet.java | 12 +-- .../restdocs/test/OperationBuilder.java | 16 ++-- .../restdocs/test/SnippetMatchers.java | 54 +++++------ .../curl-request-with-title.snippet | 0 .../http-request-with-title.snippet | 0 .../http-response-with-title.snippet | 0 .../links-with-extra-column.snippet | 0 .../links-with-title.snippet | 0 .../path-parameters-with-extra-column.snippet | 0 .../path-parameters-with-title.snippet | 0 .../request-fields-with-extra-column.snippet | 0 ...quest-fields-with-list-description.snippet | 0 .../request-fields-with-title.snippet | 0 .../request-headers-with-extra-column.snippet | 0 .../request-headers-with-title.snippet | 0 ...quest-parameters-with-extra-column.snippet | 0 .../request-parameters-with-title.snippet | 0 .../response-fields-with-extra-column.snippet | 0 .../response-fields-with-title.snippet | 0 ...response-headers-with-extra-column.snippet | 0 .../response-headers-with-title.snippet | 0 .../curl-request-with-title.snippet | 0 .../http-request-with-title.snippet | 0 .../http-response-with-title.snippet | 0 .../links-with-extra-column.snippet | 0 .../{md => markdown}/links-with-title.snippet | 0 .../path-parameters-with-extra-column.snippet | 0 .../path-parameters-with-title.snippet | 0 .../request-fields-with-extra-column.snippet | 0 .../request-fields-with-title.snippet | 0 .../request-headers-with-extra-column.snippet | 0 .../request-headers-with-title.snippet | 0 ...quest-parameters-with-extra-column.snippet | 0 .../request-parameters-with-title.snippet | 0 .../response-fields-with-extra-column.snippet | 0 .../response-fields-with-title.snippet | 0 ...response-headers-with-extra-column.snippet | 0 .../response-headers-with-title.snippet | 0 ...kMvcRestDocumentationIntegrationTests.java | 6 +- ...uredRestDocumentationIntegrationTests.java | 2 +- 94 files changed, 273 insertions(+), 244 deletions(-) delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{snippet/SnippetFormat.java => templates/TemplateFormat.java} (65%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/curl-request.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/http-request.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/http-response.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/links.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/path-parameters.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/request-fields.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/request-headers.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/request-parameters.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/response-fields.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{adoc => asciidoctor}/response-headers.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/curl-request.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/http-request.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/http-response.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/links.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/path-parameters.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/request-fields.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/request-headers.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/request-parameters.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/response-fields.snippet (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/{md => markdown}/response-headers.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/curl-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/http-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/http-response-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/links-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/links-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/path-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/path-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-fields-with-list-description.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-headers-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/request-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/response-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/response-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/response-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{adoc => asciidoctor}/response-headers-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/curl-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/http-request-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/http-response-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/links-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/links-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/path-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/path-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-headers-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-parameters-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/request-parameters-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/response-fields-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/response-fields-with-title.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/response-headers-with-extra-column.snippet (100%) rename spring-restdocs-core/src/test/resources/custom-snippet-templates/{md => markdown}/response-headers-with-title.snippet (100%) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 4c188cd45..c54f7a925 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.templates.TemplateFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index f068507e9..7941cb9ec 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -59,11 +59,11 @@ include::{examples-dir}/com/example/restassured/CustomEncoding.java[tags=custom- -[[configuration-snippet-format]] -=== Snippet format +[[configuration-snippet-template-format]] +=== Snippet template format -The default snippet format is Asciidoctor. Markdown is also supported out of the box. You -can change the default format using the `RestDocumentationConfigurer` API: +The default snippet template format is Asciidoctor. Markdown is also supported out of the +box. You can change the default format using the `RestDocumentationConfigurer` API: [source,java,indent=0,role="primary"] .MockMvc diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index ab121224a..3a32a9b53 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -22,7 +22,7 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -42,7 +42,7 @@ public void setUp() { // tag::custom-format[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation) - .snippets().withFormat(SnippetFormats.markdown())) + .snippets().withTemplateFormat(TemplateFormats.markdown())) .build(); // end::custom-format[] } diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index 649becb25..c5424ec2a 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -19,7 +19,7 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.RestDocumentation; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; @@ -38,7 +38,7 @@ public void setUp() { // tag::custom-format[] this.spec = new RequestSpecBuilder() .addFilter(documentationConfiguration(this.restDocumentation) - .snippets().withFormat(SnippetFormats.markdown())) + .snippets().withTemplateFormat(TemplateFormats.markdown())) .build(); // end::custom-format[] } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index ee721725b..f77c7aa45 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -104,7 +104,7 @@ public void apply(Map configuration, .get(SnippetConfiguration.class.getName()); engineToUse = new MustacheTemplateEngine( new StandardTemplateResourceResolver( - snippetConfiguration.getFormat())); + snippetConfiguration.getTemplateFormat())); } configuration.put(TemplateEngine.class.getName(), engineToUse); } @@ -129,7 +129,7 @@ public void apply(Map configuration, resolverToUse = new StandardWriterResolver( new RestDocumentationContextPlaceholderResolver(context), snippetConfiguration.getEncoding(), - snippetConfiguration.getFormat()); + snippetConfiguration.getTemplateFormat()); } configuration.put(WriterResolver.class.getName(), resolverToUse); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java index ecfd94e20..a1ced2167 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.config; -import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.templates.TemplateFormat; /** * An encapsulation of the configuration for documentation snippets. @@ -27,18 +27,18 @@ class SnippetConfiguration { private final String encoding; - private final SnippetFormat format; + private final TemplateFormat format; - SnippetConfiguration(String encoding, SnippetFormat format) { + SnippetConfiguration(String encoding, TemplateFormat templateFormat) { this.encoding = encoding; - this.format = format; + this.format = templateFormat; } String getEncoding() { return this.encoding; } - SnippetFormat getFormat() { + TemplateFormat getTemplateFormat() { return this.format; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index a028a7c6f..b82ee82cf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -24,8 +24,8 @@ import org.springframework.restdocs.curl.CurlDocumentation; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.SnippetFormat; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; /** * A configurer that can be used to configure the generated documentation snippets. @@ -55,14 +55,14 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer

    configuration, RestDocumentationContext context) { configuration.put(SnippetConfiguration.class.getName(), new SnippetConfiguration( - this.snippetEncoding, this.snippetFormat)); + this.snippetEncoding, this.templateFormat)); configuration.put(ATTRIBUTE_DEFAULT_SNIPPETS, this.defaultSnippets); } @@ -106,14 +106,14 @@ public T withDefaults(Snippet... defaultSnippets) { } /** - * Configures the format of the documentation snippets. + * Configures the format of the documentation snippet templates. * - * @param format the snippet format + * @param format the snippet template format * @return {@code this} */ @SuppressWarnings("unchecked") - public T withFormat(SnippetFormat format) { - this.snippetFormat = format; + public T withTemplateFormat(TemplateFormat format) { + this.templateFormat = format; return (T) this; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java deleted file mode 100644 index 8c24861a4..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormats.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2014-2016 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; - -/** - * An enumeration of the built-in snippet formats. - * - * @author Andy Wilkinson - */ -public abstract class SnippetFormats { - - private static final SnippetFormat ASCIIDOCTOR = new AsciidoctorSnippetFormat(); - - private static final SnippetFormat MARKDOWN = new MarkdownSnippetFormat(); - - private SnippetFormats() { - - } - - /** - * Returns the Asciidoctor snippet format. - * - * @return the snippet format - */ - public static SnippetFormat asciidoctor() { - return ASCIIDOCTOR; - } - - /** - * Returns the Markdown snippet format. - * - * @return the snippet format - */ - public static SnippetFormat markdown() { - return MARKDOWN; - } - - private static final class AsciidoctorSnippetFormat implements SnippetFormat { - - private static final String FILE_EXTENSION = "adoc"; - - @Override - public String getFileExtension() { - return FILE_EXTENSION; - } - - } - - private static final class MarkdownSnippetFormat implements SnippetFormat { - - private static final String FILE_EXTENSION = "md"; - - @Override - public String getFileExtension() { - return FILE_EXTENSION; - } - - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 1538da24b..cf2e0ed34 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -23,6 +23,8 @@ import java.io.Writer; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -40,7 +42,7 @@ public final class StandardWriterResolver implements WriterResolver { private String encoding = "UTF-8"; - private SnippetFormat snippetFormat; + private TemplateFormat templateFormat; /** * Creates a new {@code StandardWriterResolver} that will use the given @@ -50,29 +52,29 @@ public final class StandardWriterResolver implements WriterResolver { * * @param placeholderResolver the placeholder resolver * @deprecated since 1.1.0 in favor of - * {@link #StandardWriterResolver(PropertyPlaceholderHelper.PlaceholderResolver, String, SnippetFormat)} + * {@link #StandardWriterResolver(PropertyPlaceholderHelper.PlaceholderResolver, String, TemplateFormat)} */ @Deprecated public StandardWriterResolver(PlaceholderResolver placeholderResolver) { - this(placeholderResolver, "UTF-8", SnippetFormats.asciidoctor()); + this(placeholderResolver, "UTF-8", TemplateFormats.asciidoctor()); } /** * Creates a new {@code StandardWriterResolver} that will use the given * {@code placeholderResolver} to resolve any placeholders in the * {@code operationName}. Writers will use the given {@code encoding} and, when - * writing to a file, will use a filename appropriate for content in the given - * {@code snippetFormat}. + * writing to a file, will use a filename appropriate for content generated from + * templates in the given {@code templateFormat}. * * @param placeholderResolver the placeholder resolver * @param encoding the encoding - * @param snippetFormat the snippet format + * @param templateFormat the snippet format */ public StandardWriterResolver(PlaceholderResolver placeholderResolver, - String encoding, SnippetFormat snippetFormat) { + String encoding, TemplateFormat templateFormat) { this.placeholderResolver = placeholderResolver; this.encoding = encoding; - this.snippetFormat = snippetFormat; + this.templateFormat = templateFormat; } @Override @@ -80,7 +82,7 @@ public Writer resolve(String operationName, String snippetName, RestDocumentationContext context) throws IOException { File outputFile = resolveFile(this.propertyPlaceholderHelper.replacePlaceholders( operationName, this.placeholderResolver), snippetName + "." - + this.snippetFormat.getFileExtension(), context); + + this.templateFormat.getFileExtension(), context); if (outputFile != null) { createDirectoriesIfNecessary(outputFile); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index b8b2c62e8..22912eb36 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -18,8 +18,6 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.restdocs.snippet.SnippetFormat; -import org.springframework.restdocs.snippet.SnippetFormats; /** * Standard implementation of {@link TemplateResourceResolver}. @@ -33,28 +31,28 @@ */ public class StandardTemplateResourceResolver implements TemplateResourceResolver { - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; /** * Creates a new {@code StandardTemplateResourceResolver} that will produce default * template resources formatted with Asciidoctor. * * @deprecated since 1.1.0 in favour of - * {@link #StandardTemplateResourceResolver(SnippetFormat)} + * {@link #StandardTemplateResourceResolver(TemplateFormat)} */ @Deprecated public StandardTemplateResourceResolver() { - this(SnippetFormats.asciidoctor()); + this(TemplateFormats.asciidoctor()); } /** * Creates a new {@code StandardTemplateResourceResolver} that will produce default - * template resources formatted with the given {@code snippetFormat}. + * template resources formatted with the given {@code templateFormat}. * - * @param snippetFormat the format for the default snippet templates + * @param templateFormat the format for the default snippet templates */ - public StandardTemplateResourceResolver(SnippetFormat snippetFormat) { - this.snippetFormat = snippetFormat; + public StandardTemplateResourceResolver(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; } @Override @@ -64,8 +62,7 @@ public Resource resolveTemplateResource(String name) { if (!classPathResource.exists()) { classPathResource = new ClassPathResource( "org/springframework/restdocs/templates/" - + this.snippetFormat.getFileExtension() + "/" + name - + ".snippet"); + + this.templateFormat.getId() + "/" + name + ".snippet"); if (!classPathResource.exists()) { throw new IllegalStateException("Template named '" + name + "' could not be resolved"); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java similarity index 65% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java index 58e31f04e..972103607 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/SnippetFormat.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java @@ -14,18 +14,26 @@ * limitations under the License. */ -package org.springframework.restdocs.snippet; +package org.springframework.restdocs.templates; /** - * A {@link SnippetFormat} provides information about a particular snippet format, such as - * Asciidoctor or Markdown. + * A {@link TemplateFormat} provides information about a particular template format, such + * as Asciidoctor or Markdown. * * @author Andy Wilkinson */ -public interface SnippetFormat { +public interface TemplateFormat { /** - * Returns the snippet format's file extension. + * Returns the id of this template format. + * + * @return the id + */ + String getId(); + + /** + * Returns the file extension to use for files generated from templates in this + * format. * * @return the file extension */ diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java new file mode 100644 index 000000000..537f01ed5 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java @@ -0,0 +1,93 @@ +/* + * Copyright 2014-2016 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.templates; + +/** + * An enumeration of the built-in formats for which templates are provuded. + * + * @author Andy Wilkinson + */ +public abstract class TemplateFormats { + + private static final TemplateFormat ASCIIDOCTOR = new AsciidoctorTemplateFormat(); + + private static final TemplateFormat MARKDOWN = new MarkdownTemplateFormat(); + + private TemplateFormats() { + + } + + /** + * Returns the Asciidoctor template format with the ID {@code asciidoctor} and the + * file extension {@code adoc}. + * + * @return the template format + */ + public static TemplateFormat asciidoctor() { + return ASCIIDOCTOR; + } + + /** + * Returns the Markdown template format with the ID {@code markdown} and the file + * extension {@code md}. + * + * @return the template format + */ + public static TemplateFormat markdown() { + return MARKDOWN; + } + + private abstract static class AbstractTemplateFormat implements TemplateFormat { + + private final String name; + + private final String fileExtension; + + private AbstractTemplateFormat(String name, String fileExtension) { + this.name = name; + this.fileExtension = fileExtension; + } + + @Override + public String getId() { + return this.name; + } + + @Override + public String getFileExtension() { + return this.fileExtension; + } + + } + + private static final class AsciidoctorTemplateFormat extends AbstractTemplateFormat { + + private AsciidoctorTemplateFormat() { + super("asciidoctor", "adoc"); + } + + } + + private static final class MarkdownTemplateFormat extends AbstractTemplateFormat { + + private MarkdownTemplateFormat() { + super("markdown", "md"); + } + + } + +} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/adoc/response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/md/response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-headers.snippet diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index 83017f29d..edf0f1442 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -25,7 +25,7 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpStatus; -import org.springframework.restdocs.snippet.SnippetFormat; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import org.springframework.restdocs.test.SnippetMatchers; @@ -35,8 +35,8 @@ import org.springframework.restdocs.test.SnippetMatchers.TableMatcher; import org.springframework.web.bind.annotation.RequestMethod; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; -import static org.springframework.restdocs.snippet.SnippetFormats.markdown; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.markdown; /** * Abstract base class for testing snippet generation. @@ -46,7 +46,7 @@ @RunWith(Parameterized.class) public abstract class AbstractSnippetTests { - protected final SnippetFormat snippetFormat; + protected final TemplateFormat templateFormat; @Rule public ExpectedSnippet snippet; @@ -57,40 +57,40 @@ public static List parameters() { "Markdown", markdown() }); } - public AbstractSnippetTests(String name, SnippetFormat snippetFormat) { - this.snippet = new ExpectedSnippet(snippetFormat); - this.snippetFormat = snippetFormat; + public AbstractSnippetTests(String name, TemplateFormat templateFormat) { + this.snippet = new ExpectedSnippet(templateFormat); + this.templateFormat = templateFormat; } public CodeBlockMatcher codeBlock(String language) { - return SnippetMatchers.codeBlock(this.snippetFormat, language); + return SnippetMatchers.codeBlock(this.templateFormat, language); } public TableMatcher tableWithHeader(String... headers) { - return SnippetMatchers.tableWithHeader(this.snippetFormat, headers); + return SnippetMatchers.tableWithHeader(this.templateFormat, headers); } public TableMatcher tableWithTitleAndHeader(String title, String... headers) { - return SnippetMatchers - .tableWithTitleAndHeader(this.snippetFormat, title, headers); + return SnippetMatchers.tableWithTitleAndHeader(this.templateFormat, title, + headers); } public HttpRequestMatcher httpRequest(RequestMethod method, String uri) { - return SnippetMatchers.httpRequest(this.snippetFormat, method, uri); + return SnippetMatchers.httpRequest(this.templateFormat, method, uri); } public HttpResponseMatcher httpResponse(HttpStatus responseStatus) { - return SnippetMatchers.httpResponse(this.snippetFormat, responseStatus); + return SnippetMatchers.httpResponse(this.templateFormat, responseStatus); } public OperationBuilder operationBuilder(String name) { return new OperationBuilder(name, this.snippet.getOutputDirectory(), - this.snippetFormat); + this.templateFormat); } protected FileSystemResource snippetResource(String name) { return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + this.snippetFormat.getFileExtension() + "/" + name + ".snippet"); + + this.templateFormat.getId() + "/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 2dad00fe9..9819d8c54 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -28,10 +28,10 @@ import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import static org.hamcrest.CoreMatchers.equalTo; @@ -83,8 +83,8 @@ public void defaultConfiguration() { SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration .get(SnippetConfiguration.class.getName()); assertThat(snippetConfiguration.getEncoding(), is(equalTo("UTF-8"))); - assertThat(snippetConfiguration.getFormat(), - is(equalTo(SnippetFormats.asciidoctor()))); + assertThat(snippetConfiguration.getTemplateFormat(), + is(equalTo(TemplateFormats.asciidoctor()))); } @Test @@ -139,10 +139,10 @@ public void customSnippetEncoding() { } @Test - public void customSnippetFormat() { + public void customTemplateFormat() { RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); - this.configurer.snippets().withFormat(SnippetFormats.markdown()) + this.configurer.snippets().withTemplateFormat(TemplateFormats.markdown()) .apply(configuration, context); assertThat( configuration, @@ -150,8 +150,8 @@ public void customSnippetFormat() { instanceOf(SnippetConfiguration.class))); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration .get(SnippetConfiguration.class.getName()); - assertThat(snippetConfiguration.getFormat(), - is(equalTo(SnippetFormats.markdown()))); + assertThat(snippetConfiguration.getTemplateFormat(), + is(equalTo(TemplateFormats.markdown()))); } private static final class TestRestDocumentationConfigurer diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index f93b04674..645866c78 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -24,8 +24,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.util.Base64Utils; @@ -48,8 +48,8 @@ @RunWith(Parameterized.class) public class CurlRequestSnippetTests extends AbstractSnippetTests { - public CurlRequestSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public CurlRequestSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java index ca011d2b3..9a5a2a7e7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -23,7 +23,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -40,7 +40,7 @@ public class RequestHeadersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index ed076d263..d9732a90f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -42,8 +42,8 @@ */ public class RequestHeadersSnippetTests extends AbstractSnippetTests { - public RequestHeadersSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java index 382e91251..fae546279 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -23,7 +23,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -40,7 +40,7 @@ public class ResponseHeadersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index f58805e01..33a53cb83 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -42,8 +42,8 @@ */ public class ResponseHeadersSnippetTests extends AbstractSnippetTests { - public ResponseHeadersSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 41e90bfb5..44bd57869 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -22,8 +22,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.web.bind.annotation.RequestMethod; @@ -44,8 +44,8 @@ public class HttpRequestSnippetTests extends AbstractSnippetTests { private static final String BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - public HttpRequestSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public HttpRequestSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 96bc7be41..8d63d175a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -23,8 +23,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -42,8 +42,8 @@ */ public class HttpResponseSnippetTests extends AbstractSnippetTests { - public HttpResponseSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public HttpResponseSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index 335127cfb..ce16c6351 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -39,7 +39,7 @@ public class LinksSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index c3bd55a74..4c22d8b28 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -39,8 +39,8 @@ */ public class LinksSnippetTests extends AbstractSnippetTests { - public LinksSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public LinksSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index a56e8ed5b..000fabd60 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -22,8 +22,8 @@ import org.junit.Rule; import org.junit.Test; import org.springframework.core.io.FileSystemResource; -import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; @@ -32,7 +32,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; /** @@ -43,7 +43,7 @@ public class AsciidoctorRequestFieldsSnippetTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Test public void requestFieldsWithListDescription() throws IOException { @@ -67,8 +67,9 @@ public void requestFieldsWithListDescription() throws IOException { } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/adoc/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/asciidoctor/" + name + + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index 442616b61..f4a762ce5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -26,7 +26,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -44,7 +44,7 @@ public class RequestFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 7f9a665a6..ff0b5c95e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -23,8 +23,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -42,8 +42,8 @@ */ public class RequestFieldsSnippetTests extends AbstractSnippetTests { - public RequestFieldsSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index d664b8074..6591d714f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -26,7 +26,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -44,7 +44,7 @@ public class ResponseFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index f4e00111d..623d51491 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -23,8 +23,8 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -42,8 +42,8 @@ */ public class ResponseFieldsSnippetTests extends AbstractSnippetTests { - public ResponseFieldsSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 7f9a07b84..06310faf7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -40,7 +40,7 @@ public class PathParametersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 9d148769e..7f989a82c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -40,8 +40,8 @@ */ public class PathParametersSnippetTests extends AbstractSnippetTests { - public PathParametersSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java index b795dd572..849b0d5d7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -40,7 +40,7 @@ public class RequestParametersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(SnippetFormats.asciidoctor()); + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 2facc40c6..944db79e5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -21,8 +21,8 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -40,8 +40,8 @@ */ public class RequestParametersSnippetTests extends AbstractSnippetTests { - public RequestParametersSnippetTests(String name, SnippetFormat snippetFormat) { - super(name, snippetFormat); + public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 31bdb75dc..3f2406a53 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -26,7 +26,7 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for {@link StandardWriterResolver}. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index 00a3c8d4a..8d8ef578a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -29,7 +29,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for {@link TemplateResourceResolver}. @@ -68,8 +68,8 @@ public Resource call() { @Test public void fallsBackToDefaultSnippet() throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/adoc/test.snippet", getClass() - .getResource("test.snippet")); + "org/springframework/restdocs/templates/asciidoctor/test.snippet", + getClass().getResource("test.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 6a571f350..1bc8b49c6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -23,8 +23,8 @@ import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import org.springframework.restdocs.snippet.SnippetFormat; import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; import static org.hamcrest.CoreMatchers.is; @@ -39,7 +39,7 @@ */ public class ExpectedSnippet implements TestRule { - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; private final SnippetMatcher snippet; @@ -49,9 +49,9 @@ public class ExpectedSnippet implements TestRule { private File outputDirectory; - public ExpectedSnippet(SnippetFormat snippetFormat) { - this.snippetFormat = snippetFormat; - this.snippet = SnippetMatchers.snippet(snippetFormat); + public ExpectedSnippet(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + this.snippet = SnippetMatchers.snippet(templateFormat); } @Override @@ -65,7 +65,7 @@ private void verifySnippet() throws IOException { if (this.outputDirectory != null && this.expectedName != null) { File snippetDir = new File(this.outputDirectory, this.expectedName); File snippetFile = new File(snippetDir, this.expectedType + "." - + this.snippetFormat.getFileExtension()); + + this.templateFormat.getFileExtension()); assertThat(snippetFile, is(this.snippet)); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index e2a54b998..b9e050d93 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -37,12 +37,12 @@ import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.StandardOperation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; -import org.springframework.restdocs.snippet.SnippetFormat; -import org.springframework.restdocs.snippet.SnippetFormats; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; /** @@ -60,18 +60,18 @@ public class OperationBuilder { private final File outputDirectory; - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; private OperationRequestBuilder requestBuilder; public OperationBuilder(String name, File outputDirectory) { - this(name, outputDirectory, SnippetFormats.asciidoctor()); + this(name, outputDirectory, TemplateFormats.asciidoctor()); } - public OperationBuilder(String name, File outputDirectory, SnippetFormat snippetFormat) { + public OperationBuilder(String name, File outputDirectory, TemplateFormat templateFormat) { this.name = name; this.outputDirectory = outputDirectory; - this.snippetFormat = snippetFormat; + this.templateFormat = templateFormat; } public OperationRequestBuilder request(String uri) { @@ -92,14 +92,14 @@ public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { this.attributes.put(TemplateEngine.class.getName(), new MustacheTemplateEngine(new StandardTemplateResourceResolver( - this.snippetFormat))); + this.templateFormat))); } RestDocumentationContext context = new RestDocumentationContext(null, null, this.outputDirectory); this.attributes.put(RestDocumentationContext.class.getName(), context); this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( new RestDocumentationContextPlaceholderResolver(context), "UTF-8", - this.snippetFormat)); + this.templateFormat)); return new StandardOperation(this.name, (this.requestBuilder == null ? new OperationRequestBuilder( "http://localhost/").buildRequest() : this.requestBuilder diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index c076d0ca7..c08b937b9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -30,8 +30,8 @@ import org.hamcrest.Description; import org.hamcrest.Matcher; import org.springframework.http.HttpStatus; -import org.springframework.restdocs.snippet.SnippetFormat; -import org.springframework.restdocs.snippet.SnippetFormats; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; @@ -47,18 +47,19 @@ private SnippetMatchers() { } - public static SnippetMatcher snippet(SnippetFormat snippetFormat) { - return new SnippetMatcher(snippetFormat); + public static SnippetMatcher snippet(TemplateFormat templateFormat) { + return new SnippetMatcher(templateFormat); } - public static TableMatcher tableWithHeader(SnippetFormat format, String... headers) { + public static TableMatcher tableWithHeader(TemplateFormat format, + String... headers) { if ("adoc".equals(format.getFileExtension())) { return new AsciidoctorTableMatcher(null, headers); } return new MarkdownTableMatcher(null, headers); } - public static TableMatcher tableWithTitleAndHeader(SnippetFormat format, + public static TableMatcher tableWithTitleAndHeader(TemplateFormat format, String title, String... headers) { if ("adoc".equals(format.getFileExtension())) { return new AsciidoctorTableMatcher(title, headers); @@ -66,7 +67,7 @@ public static TableMatcher tableWithTitleAndHeader(SnippetFormat format, return new MarkdownTableMatcher(title, headers); } - public static HttpRequestMatcher httpRequest(SnippetFormat format, + public static HttpRequestMatcher httpRequest(TemplateFormat format, RequestMethod requestMethod, String uri) { if ("adoc".equals(format.getFileExtension())) { return new HttpRequestMatcher(requestMethod, uri, @@ -76,7 +77,8 @@ public static HttpRequestMatcher httpRequest(SnippetFormat format, "http"), 2); } - public static HttpResponseMatcher httpResponse(SnippetFormat format, HttpStatus status) { + public static HttpResponseMatcher httpResponse(TemplateFormat format, + HttpStatus status) { if ("adoc".equals(format.getFileExtension())) { return new HttpResponseMatcher(status, new AsciidoctorCodeBlockMatcher<>( "http"), 3); @@ -85,7 +87,7 @@ public static HttpResponseMatcher httpResponse(SnippetFormat format, HttpStatus } @SuppressWarnings({ "rawtypes" }) - public static CodeBlockMatcher codeBlock(SnippetFormat format, String language) { + public static CodeBlockMatcher codeBlock(TemplateFormat format, String language) { if ("adoc".equals(format.getFileExtension())) { return new AsciidoctorCodeBlockMatcher(language); } @@ -95,12 +97,12 @@ public static CodeBlockMatcher codeBlock(SnippetFormat format, String languag private static abstract class AbstractSnippetContentMatcher extends BaseMatcher { - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; private List lines = new ArrayList<>(); - protected AbstractSnippetContentMatcher(SnippetFormat snippetFormat) { - this.snippetFormat = snippetFormat; + protected AbstractSnippetContentMatcher(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; } protected void addLine(String line) { @@ -121,7 +123,7 @@ public boolean matches(Object item) { @Override public void describeTo(Description description) { - description.appendText(this.snippetFormat.getFileExtension() + " snippet"); + description.appendText(this.templateFormat.getFileExtension() + " snippet"); description.appendText(getLinesAsString()); } @@ -157,8 +159,8 @@ private String getLinesAsString() { public static class CodeBlockMatcher> extends AbstractSnippetContentMatcher { - protected CodeBlockMatcher(SnippetFormat snippetFormat) { - super(snippetFormat); + protected CodeBlockMatcher(TemplateFormat templateFormat) { + super(templateFormat); } @SuppressWarnings("unchecked") @@ -178,7 +180,7 @@ public static class AsciidoctorCodeBlockMatcher { protected AsciidoctorCodeBlockMatcher(String language) { - super(SnippetFormats.asciidoctor()); + super(TemplateFormats.asciidoctor()); this.addLine("[source," + language + "]"); this.addLine("----"); this.addLine("----"); @@ -195,7 +197,7 @@ public static class MarkdownCodeBlockMatcher { protected MarkdownCodeBlockMatcher(String language) { - super(SnippetFormats.markdown()); + super(TemplateFormats.markdown()); this.addLine("```" + language); this.addLine("```"); } @@ -286,8 +288,8 @@ private HttpRequestMatcher(RequestMethod requestMethod, String uri, public static abstract class TableMatcher> extends AbstractSnippetContentMatcher { - protected TableMatcher(SnippetFormat snippetFormat) { - super(snippetFormat); + protected TableMatcher(TemplateFormat templateFormat) { + super(templateFormat); } public abstract T row(String... entries); @@ -303,7 +305,7 @@ public static final class AsciidoctorTableMatcher extends TableMatcher { private AsciidoctorTableMatcher(String title, String... columns) { - super(SnippetFormats.asciidoctor()); + super(TemplateFormats.asciidoctor()); if (StringUtils.hasText(title)) { this.addLine("." + title); } @@ -340,7 +342,7 @@ public static final class MarkdownTableMatcher extends TableMatcher { private MarkdownTableMatcher(String title, String... columns) { - super(SnippetFormats.asciidoctor()); + super(TemplateFormats.asciidoctor()); if (StringUtils.hasText(title)) { this.addLine(title); } @@ -379,12 +381,12 @@ public MarkdownTableMatcher configuration(String configuration) { */ public static final class SnippetMatcher extends BaseMatcher { - private final SnippetFormat snippetFormat; + private final TemplateFormat templateFormat; private Matcher expectedContents; - private SnippetMatcher(SnippetFormat snippetFormat) { - this.snippetFormat = snippetFormat; + private SnippetMatcher(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; } @Override @@ -435,8 +437,8 @@ public void describeTo(Description description) { this.expectedContents.describeTo(description); } else { - description - .appendText(this.snippetFormat.getFileExtension() + " snippet"); + description.appendText(this.templateFormat.getFileExtension() + + " snippet"); } } diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/curl-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/curl-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/curl-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/http-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/http-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-response-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/http-response-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/http-response-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/http-response-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/links-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/links-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/links-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/links-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/links-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/path-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/path-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/path-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/path-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/path-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-list-description.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-list-description.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-list-description.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-list-description.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/adoc/response-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/curl-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/curl-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/curl-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/http-request-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-request-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/http-request-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/http-response-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/http-response-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/http-response-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/links-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/links-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/links-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/links-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/links-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/path-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/path-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/path-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/path-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/path-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-headers-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/request-parameters-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-fields-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-fields-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-fields-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-fields-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-fields-with-title.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-headers-with-extra-column.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-extra-column.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-headers-with-extra-column.snippet diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-headers-with-title.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/custom-snippet-templates/md/response-headers-with-title.snippet rename to spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-headers-with-title.snippet diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 0c48c4801..ac83d9719 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -75,8 +75,8 @@ import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; -import static org.springframework.restdocs.snippet.SnippetFormats.markdown; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.markdown; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; @@ -130,7 +130,7 @@ public void markdownSnippetGeneration() throws Exception { MockMvc mockMvc = MockMvcBuilders .webAppContextSetup(this.context) .apply(new MockMvcRestDocumentationConfigurer(this.restDocumentation) - .snippets().withEncoding("UTF-8").withFormat(markdown())).build(); + .snippets().withEncoding("UTF-8").withTemplateFormat(markdown())).build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("basic-markdown")); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index eeea60b48..ff3084d84 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -70,7 +70,7 @@ import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; -import static org.springframework.restdocs.snippet.SnippetFormats.asciidoctor; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; import static org.springframework.restdocs.test.SnippetMatchers.codeBlock; import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; From 811e7adf70c97682cc17d70a2bb31c91294128f3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Feb 2016 11:49:04 +0000 Subject: [PATCH 057/898] Add missing package-info.java for config package --- .../restdocs/config/package-info.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java new file mode 100644 index 000000000..f338eae83 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2014-2016 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. + */ + +/** + * Classes for configuring Spring REST Docs. + */ +package org.springframework.restdocs.config; + From 83b72f7962829abdc77d8eb38bae8e0ac86f22a4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 5 Feb 2016 20:57:46 +0000 Subject: [PATCH 058/898] Introduce constants for common attribute names Closes gh-193 --- .../restdocs/config/SnippetConfigurer.java | 9 ++---- .../generate/RestDocumentationGenerator.java | 29 ++++++++++++------- .../request/PathParametersSnippet.java | 3 +- .../RestDocumentationGeneratorTests.java | 3 +- .../RestDocumentationConfigurerTests.java | 11 ++++--- .../PathParametersSnippetFailureTests.java | 10 +++++-- .../request/PathParametersSnippetTests.java | 24 ++++++++++----- .../MockMvcRestDocumentationConfigurer.java | 10 ++++--- .../RestDocumentationRequestBuilders.java | 23 ++++++++------- .../RestDocumentationResultHandler.java | 4 ++- ...RestDocumentationRequestBuildersTests.java | 4 ++- ...estAssuredRestDocumentationConfigurer.java | 2 +- .../restassured/RestDocumentationFilter.java | 6 ++-- ...suredRestDocumentationConfigurerTests.java | 8 +++-- 14 files changed, 89 insertions(+), 57 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index b82ee82cf..0827c519e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -22,6 +22,7 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.templates.TemplateFormat; @@ -36,11 +37,6 @@ */ public abstract class SnippetConfigurer extends AbstractNestedConfigurer

    { - /** - * The name of the attribute that is used to hold the default snippets. - */ - public static final String ATTRIBUTE_DEFAULT_SNIPPETS = "org.springframework.restdocs.defaultSnippets"; - private List defaultSnippets = Arrays.asList( CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse()); @@ -77,7 +73,8 @@ protected SnippetConfigurer(P parent) { public void apply(Map configuration, RestDocumentationContext context) { configuration.put(SnippetConfiguration.class.getName(), new SnippetConfiguration( this.snippetEncoding, this.templateFormat)); - configuration.put(ATTRIBUTE_DEFAULT_SNIPPETS, this.defaultSnippets); + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS, + this.defaultSnippets); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java index b60f5d9fd..2a46d7e13 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Map; -import org.springframework.restdocs.config.SnippetConfigurer; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationResponse; @@ -45,6 +44,16 @@ */ public final class RestDocumentationGenerator { + /** + * Name of the operation attribute used to hold the request's URL template. + */ + public static final String ATTRIBUTE_NAME_URL_TEMPLATE = "org.springframework.restdocs.urlTemplate"; + + /** + * Name of the operation attribute used to hold the {@link List} of default snippets. + */ + public static final String ATTRIBUTE_NAME_DEFAULT_SNIPPETS = "org.springframework.restdocs.defaultSnippets"; + private final String identifier; private final OperationRequestPreprocessor requestPreprocessor; @@ -58,8 +67,8 @@ public final class RestDocumentationGenerator { private final ResponseConverter responseConverter; /** - * Creates a new {@code RestDocumentationGenerator} for the operation identified by the - * given {@code identifier}. The given {@code requestConverter} and + * Creates a new {@code RestDocumentationGenerator} for the operation identified by + * the given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that * can then be documented. The given documentation {@code snippets} will be produced. @@ -78,8 +87,8 @@ public RestDocumentationGenerator(String identifier, } /** - * Creates a new {@code RestDocumentationGenerator} for the operation identified by the - * given {@code identifier}. The given {@code requestConverter} and + * Creates a new {@code RestDocumentationGenerator} for the operation identified by + * the given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that * can then be documented. The given {@code requestPreprocessor} is applied to the @@ -101,8 +110,8 @@ public RestDocumentationGenerator(String identifier, } /** - * Creates a new {@code RestDocumentationGenerator} for the operation identified by the - * given {@code identifier}. The given {@code requestConverter} and + * Creates a new {@code RestDocumentationGenerator} for the operation identified by + * the given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that * can then be documented. The given {@code responsePreprocessor} is applied to the @@ -125,8 +134,8 @@ public RestDocumentationGenerator(String identifier, } /** - * Creates a new {@code RestDocumentationGenerator} for the operation identified by the - * given {@code identifier}. The given {@code requestConverter} and + * Creates a new {@code RestDocumentationGenerator} for the operation identified by + * the given {@code identifier}. The given {@code requestConverter} and * {@code responseConverter} are used to convert the operation's request and response * into generic {@code OperationRequest} and {@code OperationResponse} instances that * can then be documented. The given {@code requestPreprocessor} and @@ -202,7 +211,7 @@ public void addSnippets(Snippet... snippets) { private List getSnippets(Map configuration) { List combinedSnippets = new ArrayList<>(this.snippets); List defaultSnippets = (List) configuration - .get(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS); + .get(ATTRIBUTE_NAME_DEFAULT_SNIPPETS); if (defaultSnippets != null) { combinedSnippets.addAll(defaultSnippets); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 0670be1da..b09b59fab 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -23,6 +23,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.SnippetException; @@ -91,7 +92,7 @@ protected Set extractActualParameters(Operation operation) { private String extractUrlTemplate(Operation operation) { String urlTemplate = (String) operation.getAttributes().get( - "org.springframework.restdocs.urlTemplate"); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE); Assert.notNull( urlTemplate, "urlTemplate not found. If you are using MockMvc, did you use RestDocumentationRequestBuilders to " diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index d03aa6dae..a202eb350 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -25,7 +25,6 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.http.HttpHeaders; -import org.springframework.restdocs.config.SnippetConfigurer; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -91,7 +90,7 @@ public void defaultSnippetsAreCalled() throws IOException { HashMap configuration = new HashMap<>(); Snippet defaultSnippet1 = mock(Snippet.class); Snippet defaultSnippet2 = mock(Snippet.class); - configuration.put(SnippetConfigurer.ATTRIBUTE_DEFAULT_SNIPPETS, + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS, Arrays.asList(defaultSnippet1, defaultSnippet2)); new RestDocumentationGenerator<>("id", this.requestConverter, this.responseConverter, this.snippet).handle(this.request, this.response, diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 9819d8c54..a18eebc77 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -25,6 +25,7 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.curl.CurlDocumentation; import org.springframework.restdocs.curl.CurlRequestSnippet; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; import org.springframework.restdocs.snippet.Snippet; @@ -67,10 +68,11 @@ public void defaultConfiguration() { instanceOf(StandardWriterResolver.class))); assertThat( configuration, - hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + hasEntry( + equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), instanceOf(List.class))); List defaultSnippets = (List) configuration - .get("org.springframework.restdocs.defaultSnippets"); + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); assertThat( defaultSnippets, contains(instanceOf(CurlRequestSnippet.class), @@ -115,11 +117,12 @@ public void customDefaultSnippets() { .apply(configuration, context); assertThat( configuration, - hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + hasEntry( + equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), instanceOf(List.class))); @SuppressWarnings("unchecked") List defaultSnippets = (List) configuration - .get("org.springframework.restdocs.defaultSnippets"); + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 06310faf7..c389c2803 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; @@ -53,7 +54,8 @@ public void undocumentedPathParameter() throws IOException { new PathParametersSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-path-parameter", this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/") + .build()); } @Test @@ -65,7 +67,8 @@ public void missingPathParameter() throws IOException { Arrays.asList(parameterWithName("a").description("one"))) .document(new OperationBuilder("missing-path-parameter", this.snippet .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/") + .build()); } @Test @@ -79,7 +82,8 @@ public void undocumentedAndMissingPathParameters() throws IOException { .document(new OperationBuilder( "undocumented-and-missing-path-parameters", this.snippet .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{b}").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{b}") + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 7f989a82c..bb17db106 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -21,6 +21,7 @@ import org.junit.Test; import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateResourceResolver; @@ -51,9 +52,10 @@ public void pathParameters() throws IOException { "one").row("b", "two")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(operationBuilder("path-parameters") - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") - .build()); + .description("two"))) + .document(operationBuilder("path-parameters").attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}").build()); } @Test @@ -64,7 +66,8 @@ public void ignoredPathParameter() throws IOException { new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))).document(operationBuilder( "ignored-path-parameter").attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") + .build()); } @Test @@ -77,7 +80,8 @@ public void pathParametersWithQueryString() throws IOException { parameterWithName("a").description("one"), parameterWithName("b") .description("two"))).document(operationBuilder( "path-parameters-with-query-string").attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar").build()); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}?foo=bar").build()); } @Test @@ -93,7 +97,8 @@ public void pathParametersWithCustomAttributes() throws IOException { .description("two").attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))).document(operationBuilder( "path-parameters-with-custom-attributes") - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); @@ -112,8 +117,11 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), parameterWithName("b") .description("two").attributes(key("foo").value("bravo")))) - .document(operationBuilder("path-parameters-with-custom-descriptor-attributes") - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") + .document(operationBuilder( + "path-parameters-with-custom-descriptor-attributes") + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)).build()); } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index 7921cab8c..e20ecd01b 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -23,6 +23,7 @@ import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.config.RestDocumentationConfigurer; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; @@ -90,11 +91,12 @@ public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) RestDocumentationContext context = this.restDocumentation.beforeOperation(); Map configuration = new HashMap<>(); configuration.put(MockHttpServletRequest.class.getName(), request); - String urlTemplateAttribute = "org.springframework.restdocs.urlTemplate"; - configuration.put(urlTemplateAttribute, - request.getAttribute(urlTemplateAttribute)); + configuration + .put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + request.getAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE)); configuration.put(RestDocumentationContext.class.getName(), context); - request.setAttribute("org.springframework.restdocs.configuration", + request.setAttribute( + RestDocumentationResultHandler.ATTRIBUTE_NAME_CONFIGURATION, configuration); MockMvcRestDocumentationConfigurer.this.apply(configuration, context); MockMvcRestDocumentationConfigurer.this.uriConfigurer.apply(configuration, diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java index 2c9dc4529..ffe7220da 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java @@ -19,6 +19,7 @@ import java.net.URI; import org.springframework.http.HttpMethod; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.request.RequestDocumentation; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; @@ -38,8 +39,6 @@ */ public abstract class RestDocumentationRequestBuilders { - private static final String ATTRIBUTE_NAME_URL_TEMPLATE = "org.springframework.restdocs.urlTemplate"; - private RestDocumentationRequestBuilders() { } @@ -55,7 +54,7 @@ private RestDocumentationRequestBuilders() { public static MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.get(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -79,7 +78,7 @@ public static MockHttpServletRequestBuilder get(URI uri) { public static MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.post(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -103,7 +102,7 @@ public static MockHttpServletRequestBuilder post(URI uri) { public static MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.put(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -127,7 +126,7 @@ public static MockHttpServletRequestBuilder put(URI uri) { public static MockHttpServletRequestBuilder patch(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.patch(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -151,7 +150,7 @@ public static MockHttpServletRequestBuilder patch(URI uri) { public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.delete(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -175,7 +174,7 @@ public static MockHttpServletRequestBuilder delete(URI uri) { public static MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.options(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -199,7 +198,7 @@ public static MockHttpServletRequestBuilder options(URI uri) { public static MockHttpServletRequestBuilder head(String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.head(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -224,7 +223,8 @@ public static MockHttpServletRequestBuilder head(URI uri) { public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) { return MockMvcRequestBuilders.request(httpMethod, urlTemplate, urlVariables) - .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + .requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + urlTemplate); } /** @@ -250,7 +250,8 @@ public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTempla Object... urlVariables) { return (MockMultipartHttpServletRequestBuilder) MockMvcRequestBuilders .fileUpload(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + urlTemplate); } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index d536da132..ac43bdd68 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -35,6 +35,8 @@ */ public class RestDocumentationResultHandler implements ResultHandler { + static final String ATTRIBUTE_NAME_CONFIGURATION = "org.springframework.restdocs.configuration"; + private final RestDocumentationGenerator delegate; RestDocumentationResultHandler( @@ -47,7 +49,7 @@ public class RestDocumentationResultHandler implements ResultHandler { public void handle(MvcResult result) throws Exception { @SuppressWarnings("unchecked") Map configuration = (Map) result.getRequest() - .getAttribute("org.springframework.restdocs.configuration"); + .getAttribute(ATTRIBUTE_NAME_CONFIGURATION); this.delegate.handle(result.getRequest(), result.getResponse(), configuration); } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java index a5bd3159f..069687896 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java @@ -24,6 +24,7 @@ import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockServletContext; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -143,7 +144,8 @@ private void assertTemplate(MockHttpServletRequestBuilder builder, HttpMethod httpMethod) { MockHttpServletRequest request = builder.buildRequest(this.servletContext); assertThat( - (String) request.getAttribute("org.springframework.restdocs.urlTemplate"), + (String) request + .getAttribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE), is(equalTo("{template}"))); assertThat(request.getRequestURI(), is(equalTo("t"))); assertThat(request.getMethod(), is(equalTo(httpMethod.name()))); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 25afe0ec2..4702a752d 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -59,7 +59,7 @@ public Response filter(FilterableRequestSpecification requestSpec, RestDocumentationContext context = this.restDocumentation.beforeOperation(); filterContext.setValue(RestDocumentationContext.class.getName(), context); Map configuration = new HashMap<>(); - filterContext.setValue("org.springframework.restdocs.configuration", + filterContext.setValue(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION, configuration); apply(configuration, context); return filterContext.next(requestSpec, responseSpec); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 29a0b2571..fdb3b0681 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -37,6 +37,8 @@ */ public final class RestDocumentationFilter implements Filter { + static final String CONTEXT_KEY_CONFIGURATION = "org.springframework.restdocs.configuration"; + private final RestDocumentationGenerator delegate; RestDocumentationFilter( @@ -51,11 +53,11 @@ public Response filter(FilterableRequestSpecification requestSpec, Response response = context.next(requestSpec, responseSpec); Map configuration = new HashMap<>( - context.>getValue("org.springframework.restdocs.configuration")); + context.>getValue(CONTEXT_KEY_CONFIGURATION)); configuration.put(RestDocumentationContext.class.getName(), context .getValue(RestDocumentationContext.class .getName())); - configuration.put("org.springframework.restdocs.urlTemplate", + configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, requestSpec.getUserDefinedPath()); this.delegate.handle(requestSpec, response, configuration); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index 1f7bbbd44..6b4e02c68 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; @@ -62,7 +63,7 @@ public class RestAssuredRestDocumentationConfigurerTests { public void nextFilterIsCalled() { this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); verify(this.filterContext).setValue( - eq("org.springframework.restdocs.configuration"), any(Map.class)); + eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), any(Map.class)); } @Test @@ -71,7 +72,7 @@ public void configurationIsAddedToTheContext() { @SuppressWarnings("rawtypes") ArgumentCaptor configurationCaptor = ArgumentCaptor.forClass(Map.class); verify(this.filterContext).setValue( - eq("org.springframework.restdocs.configuration"), + eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), configurationCaptor.capture()); @SuppressWarnings("unchecked") Map configuration = configurationCaptor.getValue(); @@ -85,7 +86,8 @@ public void configurationIsAddedToTheContext() { instanceOf(WriterResolver.class))); assertThat( configuration, - hasEntry(equalTo("org.springframework.restdocs.defaultSnippets"), + hasEntry( + equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), instanceOf(List.class))); } } From 6578f4273069357d21d6138f2b1b97aa207763ea Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Feb 2016 13:41:24 +0000 Subject: [PATCH 059/898] Allow custom templates to be provided for a specific template format Previously, custom snippet templates were loaded from org/springframework/restdocs/templates and the default templates were loaded from org/springframework/restdocs/templates/{$formatId}. Without relying on the ordering of the classpath, this made it impossible to provide a custom template for a specific format. This commit updates the locations that are checked for snippet templates. The following locations are now checked in order: 1. org/springframework/restdocs/templates/${formatId}/${name}.snippet 2. org/springframework/restdocs/templates/${name}.snippet 3. org/springframework/restdocs/templates/${formatId}/default-${name}.snippet The second location is provided largely for backwards compatibility with 1.0. Users are expected to use the first location to provide any custom templates, with Spring REST Docs provided templates for all of the built-in snippets in the third location. Closes gh-196 Closes gh-197 --- .../docs/asciidoc/documenting-your-api.adoc | 10 +++- .../StandardTemplateResourceResolver.java | 57 ++++++++++++++----- ...t.snippet => default-curl-request.snippet} | 0 ...t.snippet => default-http-request.snippet} | 0 ....snippet => default-http-response.snippet} | 0 .../{links.snippet => default-links.snippet} | 0 ...nippet => default-path-parameters.snippet} | 0 ...snippet => default-request-fields.snippet} | 0 ...nippet => default-request-headers.snippet} | 0 ...pet => default-request-parameters.snippet} | 0 ...nippet => default-response-fields.snippet} | 0 ...ippet => default-response-headers.snippet} | 0 ...t.snippet => default-curl-request.snippet} | 0 ...t.snippet => default-http-request.snippet} | 0 ....snippet => default-http-response.snippet} | 0 .../{links.snippet => default-links.snippet} | 0 ...nippet => default-path-parameters.snippet} | 0 ...snippet => default-request-fields.snippet} | 0 ...nippet => default-request-headers.snippet} | 0 ...pet => default-request-parameters.snippet} | 0 ...nippet => default-response-fields.snippet} | 0 ...ippet => default-response-headers.snippet} | 0 ...StandardTemplateResourceResolverTests.java | 52 ++++++++++++++--- .../{test.snippet => test-custom.snippet} | 0 .../restdocs/templates/test-default.snippet | 0 .../test-format-specific-custom.snippet | 0 26 files changed, 93 insertions(+), 26 deletions(-) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{curl-request.snippet => default-curl-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{http-request.snippet => default-http-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{http-response.snippet => default-http-response.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{links.snippet => default-links.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{path-parameters.snippet => default-path-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{request-fields.snippet => default-request-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{request-headers.snippet => default-request-headers.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{request-parameters.snippet => default-request-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{response-fields.snippet => default-response-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/{response-headers.snippet => default-response-headers.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{curl-request.snippet => default-curl-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{http-request.snippet => default-http-request.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{http-response.snippet => default-http-response.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{links.snippet => default-links.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{path-parameters.snippet => default-path-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{request-fields.snippet => default-request-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{request-headers.snippet => default-request-headers.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{request-parameters.snippet => default-request-parameters.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{response-fields.snippet => default-response-fields.snippet} (100%) rename spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/{response-headers.snippet => default-response-headers.snippet} (100%) rename spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/{test.snippet => test-custom.snippet} (100%) create mode 100644 spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-default.snippet create mode 100644 spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-format-specific-custom.snippet diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index c68b9d43a..fb9977c85 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -627,10 +627,14 @@ generated snippets. templates] are provided for each of the snippets that Spring REST Docs can produce. To customize a snippet's content, you can provide your own template. -Templates are loaded from the classpath in the `org.springframework.restdocs.templates` -package and each template is named after the snippet that it will produce. For example, to +Templates are loaded from the classpath from an `org.springframework.restdocs.templates` +subpackage. The name of the subpackage is determined by the ID of the template format +that is in use. The default template format, Asciidoctor, has the ID `asciidoctor` so +snippets are loaded from `org.springframework.restdocs.templates.asciidoctor`. Each +template is named after the snippet that it will produce. For example, to override the template for the `curl-request.adoc` snippet, create a template named -`curl-request.snippet` in `src/test/resources/org/springframework/restdocs/templates`. +`curl-request.snippet` in +`src/test/resources/org/springframework/restdocs/templates/asciidoctor`. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index 22912eb36..7e0b38ac8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -22,12 +22,20 @@ /** * Standard implementation of {@link TemplateResourceResolver}. *

    - * Templates are resolved by looking for a resource on the classpath named - * {@code org/springframework/restdocs/templates/{name}.snippet}. If no such - * resource exists an attempt is made to return a default resource that is appropriate for - * the configured snippet format. + * Templates are resolved by looking for resources on the classpath. The following + * locations are checked in order: + *

      + *
    1. + * org/springframework/restdocs/templates/${templateFormatId}/${name}.snippet + *
    2. + *
    3. org/springframework/restdocs/templates/${name}.snippet
    4. + *
    5. + * org/springframework/restdocs/templates/${templateFormatId}/default-${name}.snippet + *
    6. + *
    * * @author Andy Wilkinson + * @see TemplateFormat#getId() */ public class StandardTemplateResourceResolver implements TemplateResourceResolver { @@ -57,18 +65,37 @@ public StandardTemplateResourceResolver(TemplateFormat templateFormat) { @Override public Resource resolveTemplateResource(String name) { - ClassPathResource classPathResource = new ClassPathResource( - "org/springframework/restdocs/templates/" + name + ".snippet"); - if (!classPathResource.exists()) { - classPathResource = new ClassPathResource( - "org/springframework/restdocs/templates/" - + this.templateFormat.getId() + "/" + name + ".snippet"); - if (!classPathResource.exists()) { - throw new IllegalStateException("Template named '" + name - + "' could not be resolved"); - } + Resource formatSpecificCustomTemplate = getFormatSpecificCustomTemplate(name); + if (formatSpecificCustomTemplate.exists()) { + return formatSpecificCustomTemplate; } - return classPathResource; + Resource customTemplate = getCustomTemplate(name); + if (customTemplate.exists()) { + return customTemplate; + } + Resource defaultTemplate = getDefaultTemplate(name); + if (defaultTemplate.exists()) { + return defaultTemplate; + } + throw new IllegalStateException("Template named '" + name + + "' could not be resolved"); + } + + private Resource getFormatSpecificCustomTemplate(String name) { + return new ClassPathResource(String.format( + "org/springframework/restdocs/templates/%s/%s.snippet", + this.templateFormat.getId(), name)); + } + + private Resource getCustomTemplate(String name) { + return new ClassPathResource(String.format( + "org/springframework/restdocs/templates/%s.snippet", name)); + } + + private Resource getDefaultTemplate(String name) { + return new ClassPathResource(String.format( + "org/springframework/restdocs/templates/%s/default-%s.snippet", + this.templateFormat.getId(), name)); } } diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/curl-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-curl-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/curl-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-curl-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-http-request.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-request.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-http-request.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-http-response.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/http-response.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-http-response.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/links.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/path-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/request-parameters.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-fields.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet similarity index 100% rename from spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/response-headers.snippet rename to spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index 8d8ef578a..1d152335a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -47,10 +47,17 @@ public class StandardTemplateResourceResolverTests { private final TestClassLoader classLoader = new TestClassLoader(); @Test - public void customSnippetResolution() throws Exception { + public void formatSpecificCustomSnippetHasHighestPrecedence() throws Exception { + this.classLoader.addResource( + "org/springframework/restdocs/templates/asciidoctor/test.snippet", + getClass().getResource("test-format-specific-custom.snippet")); this.classLoader.addResource( "org/springframework/restdocs/templates/test.snippet", getClass() - .getResource("test.snippet")); + .getResource("test-custom.snippet")); + this.classLoader + .addResource( + "org/springframework/restdocs/templates/asciidoctor/default-test.snippet", + getClass().getResource("test-default.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { @@ -62,14 +69,42 @@ public Resource call() { }); - assertThat(snippet.getURL(), is(equalTo(getClass().getResource("test.snippet")))); + assertThat( + snippet.getURL(), + is(equalTo(getClass().getResource("test-format-specific-custom.snippet")))); } @Test - public void fallsBackToDefaultSnippet() throws Exception { + public void generalCustomSnippetIsUsedInAbsenceOfFormatSpecificCustomSnippet() + throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/asciidoctor/test.snippet", - getClass().getResource("test.snippet")); + "org/springframework/restdocs/templates/test.snippet", getClass() + .getResource("test-custom.snippet")); + this.classLoader + .addResource( + "org/springframework/restdocs/templates/asciidoctor/default-test.snippet", + getClass().getResource("test-default.snippet")); + Resource snippet = doWithThreadContextClassLoader(this.classLoader, + new Callable() { + + @Override + public Resource call() { + return StandardTemplateResourceResolverTests.this.resolver + .resolveTemplateResource("test"); + } + + }); + + assertThat(snippet.getURL(), + is(equalTo(getClass().getResource("test-custom.snippet")))); + } + + @Test + public void defaultSnippetIsUsedInAbsenceOfCustomSnippets() throws Exception { + this.classLoader + .addResource( + "org/springframework/restdocs/templates/asciidoctor/default-test.snippet", + getClass().getResource("test-default.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { @@ -81,11 +116,12 @@ public Resource call() { }); - assertThat(snippet.getURL(), is(equalTo(getClass().getResource("test.snippet")))); + assertThat(snippet.getURL(), + is(equalTo(getClass().getResource("test-default.snippet")))); } @Test - public void failsIfCustomAndDefaultSnippetDoNotExist() throws Exception { + public void failsIfCustomAndDefaultSnippetsDoNotExist() throws Exception { this.thrown.expect(IllegalStateException.class); this.thrown.expectMessage(equalTo("Template named 'test' could not be resolved")); doWithThreadContextClassLoader(this.classLoader, new Callable() { diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-custom.snippet similarity index 100% rename from spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test.snippet rename to spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-custom.snippet diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-default.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-default.snippet new file mode 100644 index 000000000..e69de29bb diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-format-specific-custom.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/test-format-specific-custom.snippet new file mode 100644 index 000000000..e69de29bb From 0ef93074814d8d6f5ccea38acd5a5e8ed23f2266 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 8 Feb 2016 15:22:15 +0000 Subject: [PATCH 060/898] Add support for manually managing the RestDocumentationContext In 1.0, the reliance on JUnit was more widespread than it should have been. It should have been isolated to the JUnit TestRule implementation, RestDocumentation. Unfortunately, RestDocumentation was also an argument to MockMvcRestDocumentation.documentationConfiguration which made it impossible to use Spring REST Docs without having JUnit on the classpath. This commit introduces JUnitRestDocumentation and ManualRestDocumentation. The format is a direct replacement for RestDocumentation which has been reworked to delegate to JUnitRestDocumentation. The latter allows manual management of the RestDocumentationContext, primarily for use with TestNG. A new interface, RestDocumentationContextProvider, has been introduced. It is implemented by RestDocumentation, JUnitRestDocumentation and ManualRestDocumentation. MockMvcRestDocumentation.documentationConfiguration has been overridden to also accept a RestDocumentationContextProvider. The method that accepts a RestDocumentation has been deprecated, as has RestDocumentation itself. The documentation has been updated to encourage the use of JUnitRestDocumentation and a sample illustrating the use of Spring REST Docs with TestNG has been added. Closes gh-171 --- build.gradle | 4 + docs/build.gradle | 2 + docs/src/docs/asciidoc/getting-started.adoc | 73 +++++++- .../mockmvc/CustomDefaultSnippets.java | 9 +- .../com/example/mockmvc/CustomEncoding.java | 7 +- .../com/example/mockmvc/CustomFormat.java | 7 +- .../mockmvc/CustomUriConfiguration.java | 7 +- .../mockmvc/EveryTestPreprocessing.java | 3 +- .../ExampleApplicationTestNgTests.java | 58 +++++++ .../mockmvc/ExampleApplicationTests.java | 5 +- .../example/mockmvc/ParameterizedOutput.java | 3 +- .../java/com/example/mockmvc/Payload.java | 2 +- .../restassured/CustomDefaultSnippets.java | 5 +- .../example/restassured/CustomEncoding.java | 3 +- .../com/example/restassured/CustomFormat.java | 3 +- .../restassured/EveryTestPreprocessing.java | 3 +- .../ExampleApplicationTestNgTests.java | 54 ++++++ .../restassured/ExampleApplicationTests.java | 3 +- .../restassured/ParameterizedOutput.java | 3 +- .../com/example/notes/ApiDocumentation.java | 6 +- .../notes/GettingStartedDocumentation.java | 6 +- .../com/example/notes/ApiDocumentation.java | 4 +- .../notes/GettingStartedDocumentation.java | 4 +- samples/testng/build.gradle | 62 +++++++ .../testng/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + samples/testng/gradlew | 164 ++++++++++++++++++ samples/testng/gradlew.bat | 90 ++++++++++ samples/testng/src/docs/asciidoc/index.adoc | 21 +++ .../testng/SampleTestNgApplication.java | 41 +++++ .../testng/SampleTestNgApplicationTests.java | 68 ++++++++ spring-restdocs-core/build.gradle | 4 +- .../restdocs/JUnitRestDocumentation.java | 70 ++++++++ .../restdocs/ManualRestDocumentation.java | 82 +++++++++ .../restdocs/RestDocumentation.java | 42 +---- .../restdocs/RestDocumentationContext.java | 2 + .../RestDocumentationContextProvider.java | 36 ++++ spring-restdocs-mockmvc/build.gradle | 1 + .../mockmvc/MockMvcRestDocumentation.java | 20 ++- .../MockMvcRestDocumentationConfigurer.java | 20 +-- ...ckMvcRestDocumentationConfigurerTests.java | 4 +- ...kMvcRestDocumentationIntegrationTests.java | 4 +- .../RestAssuredRestDocumentation.java | 10 +- ...estAssuredRestDocumentationConfigurer.java | 11 +- ...suredRestDocumentationConfigurerTests.java | 4 +- ...uredRestDocumentationIntegrationTests.java | 4 +- 46 files changed, 932 insertions(+), 108 deletions(-) create mode 100644 docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java create mode 100644 docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java create mode 100644 samples/testng/build.gradle create mode 100644 samples/testng/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/testng/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/testng/gradlew create mode 100644 samples/testng/gradlew.bat create mode 100644 samples/testng/src/docs/asciidoc/index.adoc create mode 100644 samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java create mode 100644 samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java diff --git a/build.gradle b/build.gradle index 75fd5ae99..0e8acc927 100644 --- a/build.gradle +++ b/build.gradle @@ -162,6 +162,10 @@ samples { restNotesSpringDataRest { workingDir "$projectDir/samples/rest-notes-spring-data-rest" } + + testNg { + workingDir "$projectDir/samples/testng" + } } task api (type: Javadoc) { diff --git a/docs/build.gradle b/docs/build.gradle index 2816cd284..234d4ea4f 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -6,6 +6,8 @@ dependencies { testCompile project(':spring-restdocs-mockmvc') testCompile project(':spring-restdocs-restassured') testCompile 'javax.validation:validation-api' + testCompile 'junit:junit' + testCompile 'org.testng:testng:6.9.10' } tasks.findByPath("artifactoryPublish")?.enabled = false diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 2dc9bc92c..415836231 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -203,7 +203,7 @@ from where it will be included in the jar file. [[getting-started-documentation-snippets]] === Generating documentation snippets -Spring REST Docs uses JUnit and +Spring REST Docs uses {spring-framework-docs}/#spring-mvc-test-framework[Spring's MVC Test framework] or http://www.rest-assured.io[REST Assured] to make requests to the service that you are documenting. It then produces documentation snippets for the request and the resulting @@ -214,11 +214,18 @@ response. [[getting-started-documentation-snippets-setup]] ==== Setting up your tests -The first step in generating documentation snippets is to declare a `public` -`RestDocumentation` field that's annotated as a JUnit `@Rule`. The `RestDocumentation` -rule is configured with the output directory into which generated snippets should be -written. This output directory should match the snippets directory that you have -configured in your `build.gradle` or `pom.xml` file. +Exactly how you setup your tests depends on the test framework that you're using. +Spring REST Docs provides first-class support for JUnit. Other frameworks, such as TestNG, +are also supported although slightly more setup is required. + +[[getting-started-documentation-snippets-setup-junit]] +===== Setting up your JUnit tests + +When using JUnit, the first step in generating documentation snippets is to declare a +`public` `JUnitRestDocumentation` field that's annotated as a JUnit `@Rule`. The +`JUnitRestDocumentation` rule is configured with the output directory into which generated +snippets should be written. This output directory should match the snippets directory that +you have configured in your `build.gradle` or `pom.xml` file. For Maven (`pom.xml` that will typically be `target/generated-snippets` and for Gradle (`build.gradle`) it will typically be `build/generated-snippets`: @@ -227,14 +234,16 @@ Gradle (`build.gradle`) it will typically be `build/generated-snippets`: .Maven ---- @Rule -public RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); +public JUnitRestDocumentation restDocumentation = + new JUnitRestDocumentation("target/generated-snippets"); ---- [source,java,indent=0,role="secondary"] .Gradle ---- @Rule -public RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); +public JUnitRestDocumentation restDocumentation = + new JUnitRestDocumentation("build/generated-snippets"); ---- Next, provide an `@Before` method to configure MockMvc or REST Assured: @@ -263,6 +272,54 @@ configuration. Refer to the <> for more in +===== Setting up your tests without JUnit +[[getting-started-documentation-snippets-setup-manual]] + +The configuration when JUnit is not being used is largely similar to when it is being +used. This section describes the key differences. The {samples}/testng[TestNG sample] also +illustrates the approach. + +The first difference is that `ManualRestDocumentation` should be used in place of +`JUnitRestDocumentation` and there's no need for the `@Rule` annotation: + +[source,java,indent=0,role="primary"] +.Maven +---- +private ManualRestDocumentation restDocumentation = + new ManualRestDocumentation("target/generated-snippets"); +---- + +[source,java,indent=0,role="secondary"] +.Gradle +---- +private ManualRestDocumentation restDocumentation = + new ManualRestDocumentation("build/generated-snippets"); +---- + +Secondly, `ManualRestDocumentation.beforeTest(Class, String)` +must be called before each test. This can be done as part of the method that is +configuring MockMVC or REST Assured: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/ExampleApplicationTestNgTests.java[tags=setup] +---- + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/ExampleApplicationTestNgTests.java[tags=setup] +---- + +Lastly, `ManualRestDocumentation.afterTest` must be called after each test. For example, +with TestNG: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/restassured/ExampleApplicationTestNgTests.java[tags=teardown] +---- + [[getting-started-documentation-snippets-invoking-the-service]] ==== Invoking the RESTful service diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index fcd6a742a..4be61c375 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -16,21 +16,22 @@ package com.example.mockmvc; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; - import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + public class CustomDefaultSnippets { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java index 80130ef18..9352ca95e 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -16,20 +16,21 @@ package com.example.mockmvc; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; - import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + public class CustomEncoding { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index 3a32a9b53..2415bd40c 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -16,21 +16,22 @@ package com.example.mockmvc; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; - import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + public class CustomFormat { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index 9fe6130c9..b091199a8 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -16,20 +16,21 @@ package com.example.mockmvc; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; - import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + public class CustomUriConfiguration { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index e1f9b9961..85e7e9979 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.test.web.servlet.MockMvc; @@ -38,7 +39,7 @@ public class EveryTestPreprocessing { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "target/generated-snippets"); private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java new file mode 100644 index 000000000..0018c467b --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2016 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 com.example.mockmvc; + +import java.lang.reflect.Method; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.ManualRestDocumentation; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; + +public class ExampleApplicationTestNgTests { + + public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( + "target/generated-snippets"); + // tag::setup[] + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeMethod + public void setUp(Method method) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); + } + + // end::setup[] + + // tag::teardown[] + @AfterMethod + public void tearDown() { + this.restDocumentation.afterTest(); + } + // end::teardown[] + +} diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index 97abfa059..a27725f04 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -19,7 +19,7 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -29,7 +29,7 @@ public class ExampleApplicationTests { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "target/generated-snippets"); // tag::setup[] @Autowired @@ -44,4 +44,5 @@ public void setUp() { .build(); } // end::setup[] + } diff --git a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index ea6313cd1..c5e6a362a 100644 --- a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -29,7 +30,7 @@ public class ParameterizedOutput { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); private MockMvc mockMvc; diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index bed9663f1..4424b1f31 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -32,7 +32,7 @@ public class Payload { -private MockMvc mockMvc; + private MockMvc mockMvc; public void response() throws Exception { // tag::response[] diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 4166c13e0..ae581cd05 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -29,9 +30,9 @@ public class CustomDefaultSnippets { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); - RequestSpecification spec; + private RequestSpecification spec; @Before public void setUp() { diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java index eb85c8059..09039a448 100644 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -28,7 +29,7 @@ public class CustomEncoding { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index c5424ec2a..36ba445d4 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; @@ -29,7 +30,7 @@ public class CustomFormat { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index 68b8a88b1..a4ea45880 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.restassured.RestDocumentationFilter; @@ -38,7 +39,7 @@ public class EveryTestPreprocessing { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "target/generated-snippets"); // tag::setup[] diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java new file mode 100644 index 000000000..a28e54aec --- /dev/null +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import java.lang.reflect.Method; + +import org.springframework.restdocs.ManualRestDocumentation; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +public class ExampleApplicationTestNgTests { + + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( + "build/generated-snippets"); + + // tag::setup[] + private RequestSpecification spec; + + @BeforeMethod + public void setUp(Method method) { + this.spec = new RequestSpecBuilder().addFilter( + documentationConfiguration(this.restDocumentation)) + .build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); + } + + // end::setup[] + + // tag::teardown[] + @AfterMethod + public void tearDown() { + this.restDocumentation.afterTest(); + } + // end::teardown[] +} diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java index c0943a777..33840cb70 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -28,7 +29,7 @@ public class ExampleApplicationTests { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); // tag::setup[] diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java index 10fd8be1d..29c24835a 100644 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -29,7 +30,7 @@ public class ParameterizedOutput { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation( + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); private RequestSpecification spec; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index 51581811d..cbcaeb3fc 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -20,8 +20,8 @@ import static org.hamcrest.Matchers.notNullValue; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -45,7 +45,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -61,7 +61,7 @@ public class ApiDocumentation { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); @Autowired private NoteRepository noteRepository; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index 3770023ba..29418eb1c 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -19,8 +19,8 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; @@ -40,7 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -58,7 +58,7 @@ public class GettingStartedDocumentation { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); @Autowired private ObjectMapper objectMapper; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 4da12b336..90abbdc62 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -51,7 +51,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.constraints.ConstraintDescriptions; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; @@ -71,7 +71,7 @@ public class ApiDocumentation { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); private RestDocumentationResultHandler document; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 60ba09692..9531e5831 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -43,7 +43,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.hateoas.MediaTypes; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; @@ -61,7 +61,7 @@ public class GettingStartedDocumentation { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); @Autowired private ObjectMapper objectMapper; diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle new file mode 100644 index 000000000..a6eeb77af --- /dev/null +++ b/samples/testng/build.gradle @@ -0,0 +1,62 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + } +} + +plugins { + id "org.asciidoctor.convert" version "1.5.3" +} + +apply plugin: 'java' +apply plugin: 'spring-boot' +apply plugin: 'eclipse' + +repositories { + mavenLocal() + maven { url 'https://repo.spring.io/libs-snapshot' } + mavenCentral() +} + +group = 'com.example' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +ext { + snippetsDir = file('build/generated-snippets') + springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' +} + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-web' + testCompile('org.springframework:spring-test') { + exclude group: 'junit', module: 'junit;' + } + testCompile 'org.testng:testng:6.9.10' + testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" +} + +test { + useTestNG() + outputs.dir snippetsDir +} + +asciidoctor { + attributes 'snippets': snippetsDir + inputs.dir snippetsDir + dependsOn test +} + +jar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } +} + +eclipseJdt.onlyIf { false } +cleanEclipseJdt.onlyIf { false } diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.jar b/samples/testng/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..3d0dee6e8edfecc92e04653ec780de06f7b34f8b GIT binary patch literal 51017 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 H-~Rf4?cF@p literal 0 HcmV?d00001 diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..2b8cbfe8d --- /dev/null +++ b/samples/testng/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Apr 16 12:33:24 BST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip diff --git a/samples/testng/gradlew b/samples/testng/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/samples/testng/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/testng/gradlew.bat b/samples/testng/gradlew.bat new file mode 100644 index 000000000..8a0b282aa --- /dev/null +++ b/samples/testng/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/testng/src/docs/asciidoc/index.adoc b/samples/testng/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..ff513a31a --- /dev/null +++ b/samples/testng/src/docs/asciidoc/index.adoc @@ -0,0 +1,21 @@ += Spring REST Docs TestNG Sample +Andy Wilkinson; +:doctype: book +:icons: font +:source-highlighter: highlightjs + +This sample application demonstrates how to use Spring REST Docs with TestNG. +`SampleTestNgApplication` makes a call to a very simple service and produces three +documentation snippets. + +One showing how to make a request using cURL: + +include::{snippets}/sample/curl-request.adoc[] + +One showing the HTTP request: + +include::{snippets}/sample/http-request.adoc[] + +And one showing the HTTP response: + +include::{snippets}/sample/http-response.adoc[] \ No newline at end of file diff --git a/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java b/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java new file mode 100644 index 000000000..37674b96b --- /dev/null +++ b/samples/testng/src/main/java/com/example/testng/SampleTestNgApplication.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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 com.example.testng; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +public class SampleTestNgApplication { + + public static void main(String[] args) { + new SpringApplication(SampleTestNgApplication.class).run(args); + } + + @RestController + private static class SampleController { + + @RequestMapping("/") + public String index() { + return "Hello, World"; + } + + } + +} diff --git a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java new file mode 100644 index 000000000..b49883b25 --- /dev/null +++ b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2016 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 com.example.testng; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.lang.reflect.Method; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.restdocs.ManualRestDocumentation; +import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +@SpringApplicationConfiguration(classes=SampleTestNgApplication.class) +@WebAppConfiguration +public class SampleTestNgApplicationTests extends AbstractTestNGSpringContextTests { + + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation("build/generated-snippets"); + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @BeforeMethod + public void setUp(Method method) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + this.restDocumentation.beforeTest(getClass(), method.getName()); + } + + @AfterMethod + public void tearDown() { + this.restDocumentation.afterTest(); + } + + @Test + public void sample() throws Exception { + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(document("sample")); + } + +} diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 2ba4b30bb..9f0934756 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -26,14 +26,14 @@ task jmustacheRepackJar(type: Jar) { repackJar -> dependencies { compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'junit:junit' compile 'org.springframework:spring-webmvc' compile 'javax.servlet:javax.servlet-api' compile files(jmustacheRepackJar) jarjar 'com.googlecode.jarjar:jarjar:1.3' jmustache 'com.samskivert:jmustache@jar' - optional 'javax.validation:validation-api' optional 'commons-codec:commons-codec' + optional 'javax.validation:validation-api' + optional 'junit:junit' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java new file mode 100644 index 000000000..67bd5c0d6 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java @@ -0,0 +1,70 @@ +/* + * Copyright 2014-2016 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; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * A JUnit {@link TestRule} used to automatically manage the + * {@link RestDocumentationContext}. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public class JUnitRestDocumentation implements RestDocumentationContextProvider, TestRule { + + private final ManualRestDocumentation delegate; + + /** + * Creates a new {@code JUnitRestDocumentation} instance that will generate snippets + * to the given {@code outputDirectory}. + * + * @param outputDirectory the output directory + */ + public JUnitRestDocumentation(String outputDirectory) { + this.delegate = new ManualRestDocumentation(outputDirectory); + } + + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + + @Override + public void evaluate() throws Throwable { + Class testClass = description.getTestClass(); + String methodName = description.getMethodName(); + JUnitRestDocumentation.this.delegate.beforeTest(testClass, methodName); + try { + base.evaluate(); + } + finally { + JUnitRestDocumentation.this.delegate.afterTest(); + } + } + + }; + + } + + @Override + public RestDocumentationContext beforeOperation() { + return this.delegate.beforeOperation(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java new file mode 100644 index 000000000..ff12d5910 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2016 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; + +import java.io.File; + +/** + * {@code ManualRestDocumentation} is used to manually manage the + * {@link RestDocumentationContext}. Primarly intended for use with TestNG, but suitable + * for use in any environment where manual management of the context is required. + *

    + * Users of JUnit should use {@link JUnitRestDocumentation} and take advantage of its + * Rule-based support for automatic management of the context. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public final class ManualRestDocumentation implements RestDocumentationContextProvider { + + private final File outputDirectory; + + private RestDocumentationContext context; + + /** + * Creates a new {@code ManualRestDocumentation} instance that will generate snippets + * to the given {@code outputDirectory}. + * + * @param outputDirectory the output directory + */ + public ManualRestDocumentation(String outputDirectory) { + this.outputDirectory = new File(outputDirectory); + } + + /** + * Notification that a test is about to begin. Creates a + * {@link RestDocumentationContext} for the test on the given {@code testClass} with + * the given {@code testMethodName}. Must be followed by a call to + * {@link #afterTest()} once the test has completed. + * + * @param testClass the test class + * @param testMethodName the name of the test method + * @throws IllegalStateException if a context has already be created + */ + @SuppressWarnings("deprecation") + public void beforeTest(Class testClass, String testMethodName) { + if (this.context != null) { + throw new IllegalStateException( + "Context already exists. Did you forget to call afterTest()?"); + } + this.context = new RestDocumentationContext(testClass, testMethodName, + this.outputDirectory); + } + + /** + * Notification that a test has completed. Clears the {@link RestDocumentationContext} + * that was previously established by a call to {@link #beforeTest(Class, String)}. + */ + public void afterTest() { + this.context = null; + } + + @Override + public RestDocumentationContext beforeOperation() { + this.context.getAndIncrementStepCount(); + return this.context; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java index 401b67139..51c96aca2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -16,8 +16,6 @@ package org.springframework.restdocs; -import java.io.File; - import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -27,13 +25,12 @@ * JUnit tests. * * @author Andy Wilkinson - * + * @deprecated Since 1.1 in favor of {@link JUnitRestDocumentation} */ -public class RestDocumentation implements TestRule { - - private final String outputDirectory; +@Deprecated +public class RestDocumentation implements TestRule, RestDocumentationContextProvider { - private RestDocumentationContext context; + private final JUnitRestDocumentation delegate; /** * Creates a new {@code RestDocumentation} instance that will generate snippets to the @@ -42,40 +39,17 @@ public class RestDocumentation implements TestRule { * @param outputDirectory the output directory */ public RestDocumentation(String outputDirectory) { - this.outputDirectory = outputDirectory; + this.delegate = new JUnitRestDocumentation(outputDirectory); } @Override public Statement apply(final Statement base, final Description description) { - return new Statement() { - - @Override - public void evaluate() throws Throwable { - Class testClass = description.getTestClass(); - String methodName = description.getMethodName(); - RestDocumentation.this.context = new RestDocumentationContext(testClass, - methodName, new File(RestDocumentation.this.outputDirectory)); - try { - base.evaluate(); - } - finally { - RestDocumentation.this.context = null; - } - } - - }; - + return this.delegate.apply(base, description); } - /** - * Notification that a RESTful operation that should be documented is about to be - * performed. Returns a {@link RestDocumentationContext} for the operation. - * - * @return the context for the operation - */ + @Override public RestDocumentationContext beforeOperation() { - this.context.getAndIncrementStepCount(); - return this.context; + return this.delegate.beforeOperation(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java index 86dbf6c8e..d76fc2203 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContext.java @@ -43,7 +43,9 @@ public final class RestDocumentationContext { * @param testClass the class whose test is being executed * @param testMethodName the name of the test method that is being executed * @param outputDirectory the directory to which documentation should be written. + * @deprecated Since 1.1 in favor of {@link ManualRestDocumentation}. */ + @Deprecated public RestDocumentationContext(Class testClass, String testMethodName, File outputDirectory) { this.testClass = testClass; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java new file mode 100644 index 000000000..5dbe7a78c --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2016 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; + +/** + * A {@code RestDocumentationContextProvider} is used to provide access to the + * {@link RestDocumentationContext}. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public interface RestDocumentationContextProvider { + + /** + * Returns a {@link RestDocumentationContext} for the operation that is about to be + * performed. + * + * @return the context for the operation + */ + RestDocumentationContext beforeOperation(); + +} diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index ba3ab171b..b93fba78d 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -3,6 +3,7 @@ description = 'Spring REST Docs MockMvc' dependencies { compile 'org.springframework:spring-test' compile project(':spring-restdocs-core') + optional 'junit:junit' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index cf1840cbe..070668fd8 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.mockmvc; import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; @@ -31,6 +32,7 @@ * * @author Andy Wilkinson */ +@SuppressWarnings("deprecation") public abstract class MockMvcRestDocumentation { private static final MockMvcRequestConverter REQUEST_CONVERTER = new MockMvcRequestConverter(); @@ -48,10 +50,26 @@ private MockMvcRestDocumentation() { * @param restDocumentation the REST documentation * @return the configurer * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) + * @deprecated Since 1.1 in favor of + * {@link #documentationConfiguration(RestDocumentationContextProvider)} */ + @Deprecated public static MockMvcRestDocumentationConfigurer documentationConfiguration( RestDocumentation restDocumentation) { - return new MockMvcRestDocumentationConfigurer(restDocumentation); + return documentationConfiguration((RestDocumentationContextProvider) restDocumentation); + } + + /** + * Provides access to a {@link MockMvcConfigurer} that can be used to configure a + * {@link MockMvc} instance using the given {@code contextProvider}. + * + * @param contextProvider the context provider + * @return the configurer + * @see ConfigurableMockMvcBuilder#apply(MockMvcConfigurer) + */ + public static MockMvcRestDocumentationConfigurer documentationConfiguration( + RestDocumentationContextProvider contextProvider) { + return new MockMvcRestDocumentationConfigurer(contextProvider); } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index e20ecd01b..bbe92f987 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -20,8 +20,8 @@ import java.util.Map; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.config.RestDocumentationConfigurer; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.test.web.servlet.request.RequestPostProcessor; @@ -44,11 +44,10 @@ public class MockMvcRestDocumentationConfigurer private final UriConfigurer uriConfigurer = new UriConfigurer(this); - private final RestDocumentation restDocumentation; + private final RestDocumentationContextProvider contextManager; - MockMvcRestDocumentationConfigurer(RestDocumentation restDocumentation) { - super(); - this.restDocumentation = restDocumentation; + MockMvcRestDocumentationConfigurer(RestDocumentationContextProvider contextManager) { + this.contextManager = contextManager; } /** @@ -64,7 +63,7 @@ public UriConfigurer uris() { @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { - return new ConfigurerApplyingRequestPostProcessor(this.restDocumentation); + return new ConfigurerApplyingRequestPostProcessor(this.contextManager); } @Override @@ -80,15 +79,16 @@ public MockMvcSnippetConfigurer snippets() { private final class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { - private final RestDocumentation restDocumentation; + private final RestDocumentationContextProvider contextManager; - private ConfigurerApplyingRequestPostProcessor(RestDocumentation restDocumentation) { - this.restDocumentation = restDocumentation; + private ConfigurerApplyingRequestPostProcessor( + RestDocumentationContextProvider contextManager) { + this.contextManager = contextManager; } @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - RestDocumentationContext context = this.restDocumentation.beforeOperation(); + RestDocumentationContext context = this.contextManager.beforeOperation(); Map configuration = new HashMap<>(); configuration.put(MockHttpServletRequest.class.getName(), request); configuration diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java index ad6ac2691..bcf2dd05a 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java @@ -22,7 +22,7 @@ import org.junit.Test; import org.springframework.hateoas.mvc.BasicLinkBuilder; import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -43,7 +43,7 @@ public class MockMvcRestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); @Rule - public RestDocumentation restDocumentation = new RestDocumentation("test"); + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("test"); @Test public void defaultConfiguration() { diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index ac83d9719..368f96018 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -36,7 +36,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.hypermedia.Link; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration; import org.springframework.test.context.ContextConfiguration; @@ -96,7 +96,7 @@ public class MockMvcRestDocumentationIntegrationTests { @Rule - public RestDocumentation restDocumentation = new RestDocumentation( + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); @Autowired diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index 6f2c3f4ce..b9c53742f 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -16,7 +16,7 @@ package org.springframework.restdocs.restassured; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; @@ -104,14 +104,14 @@ public static RestDocumentationFilter document(String identifier, /** * Provides access to a {@link RestAssuredRestDocumentationConfigurer} that can be - * used to configure Spring REST Docs using the given {@code restDocumentation}. + * used to configure Spring REST Docs using the given {@code contextProvider}. * - * @param restDocumentation the REST documentation + * @param contextProvider the context provider * @return the configurer */ public static RestAssuredRestDocumentationConfigurer documentationConfiguration( - RestDocumentation restDocumentation) { - return new RestAssuredRestDocumentationConfigurer(restDocumentation); + RestDocumentationContextProvider contextProvider) { + return new RestAssuredRestDocumentationConfigurer(contextProvider); } } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 4702a752d..f655bb95e 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -19,8 +19,8 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.config.RestDocumentationConfigurer; import com.jayway.restassured.filter.Filter; @@ -42,10 +42,11 @@ public final class RestAssuredRestDocumentationConfigurer private final RestAssuredSnippetConfigurer snippetConfigurer = new RestAssuredSnippetConfigurer( this); - private final RestDocumentation restDocumentation; + private final RestDocumentationContextProvider contextProvider; - RestAssuredRestDocumentationConfigurer(RestDocumentation restDocumentation) { - this.restDocumentation = restDocumentation; + RestAssuredRestDocumentationConfigurer( + RestDocumentationContextProvider contextProvider) { + this.contextProvider = contextProvider; } @Override @@ -56,7 +57,7 @@ public RestAssuredSnippetConfigurer snippets() { @Override public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext filterContext) { - RestDocumentationContext context = this.restDocumentation.beforeOperation(); + RestDocumentationContext context = this.contextProvider.beforeOperation(); filterContext.setValue(RestDocumentationContext.class.getName(), context); Map configuration = new HashMap<>(); filterContext.setValue(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION, diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index 6b4e02c68..81ed3cee5 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -22,7 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; @@ -48,7 +48,7 @@ public class RestAssuredRestDocumentationConfigurerTests { @Rule - public final RestDocumentation restDocumentation = new RestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); private final FilterableRequestSpecification requestSpec = mock(FilterableRequestSpecification.class); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index ff3084d84..6717f8c60 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -36,7 +36,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.restdocs.RestDocumentation; +import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.hypermedia.Link; import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -88,7 +88,7 @@ public class RestAssuredRestDocumentationIntegrationTests { @Rule - public RestDocumentation restDocumentation = new RestDocumentation( + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); @Value("${local.server.port}") From 74f9e272fd5af48c30256d4bf6f3e4fc5abe2eb8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 12 Feb 2016 20:57:53 +0000 Subject: [PATCH 061/898] Apply Eclipse Mars' code formatting --- config/checkstyle/checkstyle.xml | 2 +- config/eclipse/org.eclipse.jdt.core.prefs | 4 +- config/eclipse/org.eclipse.jdt.ui.prefs | 4 +- .../restdocs/config/package-info.java | 1 - .../constraints/ConstraintDescriptions.java | 7 +- ...ceBundleConstraintDescriptionResolver.java | 12 +- .../ValidatorConstraintResolver.java | 7 +- .../restdocs/constraints/package-info.java | 1 - .../restdocs/curl/CurlRequestSnippet.java | 14 +- .../restdocs/curl/QueryStringParser.java | 8 +- .../restdocs/curl/package-info.java | 1 - .../restdocs/http/HttpRequestSnippet.java | 13 +- .../restdocs/http/package-info.java | 1 - .../hypermedia/AbstractJsonLinkExtractor.java | 8 +- .../hypermedia/HypermediaDocumentation.java | 2 +- .../restdocs/hypermedia/Link.java | 4 +- .../restdocs/hypermedia/LinksSnippet.java | 10 +- .../restdocs/hypermedia/package-info.java | 1 - .../operation/AbstractOperationMessage.java | 4 +- .../operation/OperationRequestFactory.java | 11 +- .../operation/OperationResponseFactory.java | 14 +- .../operation/StandardOperationRequest.java | 4 +- .../StandardOperationRequestPart.java | 4 +- .../operation/StandardOperationResponse.java | 4 +- .../restdocs/operation/package-info.java | 5 +- ...ContentModifyingOperationPreprocessor.java | 4 +- .../LinkMaskingContentModifier.java | 4 +- .../operation/preprocess/Preprocessors.java | 10 +- .../PrettyPrintingContentModifier.java | 21 +- .../operation/preprocess/package-info.java | 1 - .../restdocs/package-info.java | 1 - .../payload/AbstractFieldsSnippet.java | 7 +- .../restdocs/payload/JsonContentHandler.java | 8 +- .../restdocs/payload/JsonFieldPath.java | 4 +- .../restdocs/payload/JsonFieldProcessor.java | 20 +- .../restdocs/payload/XmlContentHandler.java | 13 +- .../restdocs/payload/package-info.java | 1 - .../request/AbstractParametersSnippet.java | 9 +- .../request/PathParametersSnippet.java | 4 +- .../restdocs/request/package-info.java | 1 - .../restdocs/snippet/IgnorableDescriptor.java | 4 +- ...cumentationContextPlaceholderResolver.java | 5 +- .../snippet/StandardWriterResolver.java | 6 +- .../restdocs/snippet/TemplatedSnippet.java | 8 +- .../restdocs/snippet/package-info.java | 1 - .../StandardTemplateResourceResolver.java | 7 +- .../mustache/MustacheTemplateEngine.java | 4 +- .../templates/mustache/package-info.java | 1 - .../restdocs/templates/package-info.java | 1 - .../ConstraintDescriptionsTests.java | 3 +- ...dleConstraintDescriptionResolverTests.java | 59 ++-- .../ValidatorConstraintResolverTests.java | 6 +- .../curl/CurlRequestSnippetTests.java | 279 +++++++-------- .../restdocs/curl/QueryStringParserTests.java | 21 +- .../headers/RequestHeadersSnippetTests.java | 139 ++++---- .../headers/ResponseHeadersSnippetTests.java | 138 ++++---- .../http/HttpRequestSnippetTests.java | 196 ++++++----- .../http/HttpResponseSnippetTests.java | 64 ++-- .../ContentTypeLinkExtractorTests.java | 5 +- .../LinkExtractorsPayloadTests.java | 4 +- .../hypermedia/LinksSnippetTests.java | 128 ++++--- ...ntModifyingOperationPreprocessorTests.java | 1 + ...tingOperationRequestPreprocessorTests.java | 11 +- ...ingOperationResponsePreprocessorTests.java | 15 +- ...derRemovingOperationPreprocessorTests.java | 1 + .../LinkMaskingContentModifierTests.java | 20 +- .../PatternReplacingContentModifierTests.java | 6 +- .../PrettyPrintingContentModifierTests.java | 21 +- .../payload/JsonFieldProcessorTests.java | 34 +- .../payload/JsonFieldTypeResolverTests.java | 22 +- .../payload/RequestFieldsSnippetTests.java | 261 +++++++------- .../payload/ResponseFieldsSnippetTests.java | 332 ++++++++++-------- .../request/PathParametersSnippetTests.java | 132 ++++--- .../RequestParametersSnippetTests.java | 119 ++++--- ...tationContextPlaceholderResolverTests.java | 14 +- .../snippet/StandardWriterResolverTests.java | 25 +- .../snippet/TemplatedSnippetTests.java | 7 +- ...StandardTemplateResourceResolverTests.java | 9 +- .../restdocs/test/ExpectedSnippet.java | 5 +- .../restdocs/test/OperationBuilder.java | 9 +- .../restdocs/test/SnippetMatchers.java | 34 +- .../MockMvcOperationRequestFactory.java | 30 +- .../RestDocumentationMockMvcConfigurer.java | 15 +- .../RestDocumentationRequestBuilders.java | 32 +- .../RestDocumentationResultHandler.java | 20 +- .../restdocs/mockmvc/SnippetConfigurer.java | 9 +- .../restdocs/mockmvc/UriConfigurer.java | 4 +- .../restdocs/mockmvc/package-info.java | 1 - .../MockMvcOperationRequestFactoryTests.java | 38 +- ...kMvcRestDocumentationIntegrationTests.java | 170 ++++----- .../RestDocumentationConfigurerTests.java | 17 +- ...RestDocumentationRequestBuildersTests.java | 1 + 92 files changed, 1450 insertions(+), 1298 deletions(-) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index b7908d746..d9cdead77 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -81,7 +81,7 @@ - + diff --git a/config/eclipse/org.eclipse.jdt.core.prefs b/config/eclipse/org.eclipse.jdt.core.prefs index 462feac8f..62ded11bf 100644 --- a/config/eclipse/org.eclipse.jdt.core.prefs +++ b/config/eclipse/org.eclipse.jdt.core.prefs @@ -10,7 +10,6 @@ org.eclipse.jdt.core.codeComplete.staticFieldSuffixes= org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes= org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes= org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.7 @@ -384,8 +383,9 @@ org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true org.eclipse.jdt.core.formatter.tabulation.char=tab org.eclipse.jdt.core.formatter.tabulation.size=4 -org.eclipse.jdt.core.formatter.use_on_off_tags=false +org.eclipse.jdt.core.formatter.use_on_off_tags=true org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.javaFormatter=org.springframework.ide.eclipse.jdt.formatter.javaformatter diff --git a/config/eclipse/org.eclipse.jdt.ui.prefs b/config/eclipse/org.eclipse.jdt.ui.prefs index 681d02ad0..429b30236 100644 --- a/config/eclipse/org.eclipse.jdt.ui.prefs +++ b/config/eclipse/org.eclipse.jdt.ui.prefs @@ -62,9 +62,9 @@ editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true formatter_profile=_Spring REST Docs Java Conventions formatter_settings_version=12 org.eclipse.jdt.ui.exception.name=e -org.eclipse.jdt.ui.gettersetter.use.is=false +org.eclipse.jdt.ui.gettersetter.use.is=true org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=java;javax;org;com;;\#; +org.eclipse.jdt.ui.importorder=java;javax;;org.springframework;\#; org.eclipse.jdt.ui.javadoc=true org.eclipse.jdt.ui.keywordthis=false org.eclipse.jdt.ui.ondemandthreshold=9999 diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java index f338eae83..1b4a13e01 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/package-info.java @@ -18,4 +18,3 @@ * Classes for configuring Spring REST Docs. */ package org.springframework.restdocs.config; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java index 7fc11026d..c55857980 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ConstraintDescriptions.java @@ -56,7 +56,8 @@ public ConstraintDescriptions(Class clazz) { * @param constraintResolver the constraint resolver */ public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResolver) { - this(clazz, constraintResolver, new ResourceBundleConstraintDescriptionResolver()); + this(clazz, constraintResolver, + new ResourceBundleConstraintDescriptionResolver()); } /** @@ -95,8 +96,8 @@ public ConstraintDescriptions(Class clazz, ConstraintResolver constraintResol * @return the list of constraint descriptions */ public List descriptionsForProperty(String property) { - List constraints = this.constraintResolver.resolveForProperty( - property, this.clazz); + List constraints = this.constraintResolver + .resolveForProperty(property, this.clazz); List descriptions = new ArrayList<>(); for (Constraint constraint : constraints) { descriptions.add(this.descriptionResolver.resolveDescription(constraint)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java index 660ed4b5f..759950a64 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java @@ -64,8 +64,8 @@ * * @author Andy Wilkinson */ -public class ResourceBundleConstraintDescriptionResolver implements - ConstraintDescriptionResolver { +public class ResourceBundleConstraintDescriptionResolver + implements ConstraintDescriptionResolver { private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( "${", "}"); @@ -99,8 +99,8 @@ private static ResourceBundle getBundle(String name) { try { return ResourceBundle.getBundle( ResourceBundleConstraintDescriptionResolver.class.getPackage() - .getName() + "." + name, Locale.getDefault(), Thread - .currentThread().getContextClassLoader()); + .getName() + "." + name, + Locale.getDefault(), Thread.currentThread().getContextClassLoader()); } catch (MissingResourceException ex) { return null; @@ -126,8 +126,8 @@ private String getDescription(String key) { return this.defaultDescriptions.getString(key); } - private static final class ConstraintPlaceholderResolver implements - PlaceholderResolver { + private static final class ConstraintPlaceholderResolver + implements PlaceholderResolver { private final Constraint constraint; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java index 664691c65..531157b2a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ValidatorConstraintResolver.java @@ -70,10 +70,9 @@ public List resolveForProperty(String property, Class clazz) { if (propertyDescriptor != null) { for (ConstraintDescriptor constraintDescriptor : propertyDescriptor .getConstraintDescriptors()) { - constraints - .add(new Constraint(constraintDescriptor.getAnnotation() - .annotationType().getName(), constraintDescriptor - .getAttributes())); + constraints.add(new Constraint( + constraintDescriptor.getAnnotation().annotationType().getName(), + constraintDescriptor.getAttributes())); } } return constraints; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java index 9fc48616d..fb1e842ec 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/package-info.java @@ -18,4 +18,3 @@ * Documenting a RESTful API's constraints. */ package org.springframework.restdocs.constraints; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 3e903f377..4f4ea07b7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -48,6 +48,7 @@ public class CurlRequestSnippet extends TemplatedSnippet { private static final Set HEADER_FILTERS; + static { Set headerFilters = new HashSet<>(); headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); @@ -102,7 +103,8 @@ private void writeIncludeHeadersInOutputOption(PrintWriter writer) { writer.print("-i"); } - private void writeUserOptionIfNecessary(OperationRequest request, PrintWriter writer) { + private void writeUserOptionIfNecessary(OperationRequest request, + PrintWriter writer) { List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { String credentials = BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); @@ -110,7 +112,8 @@ private void writeUserOptionIfNecessary(OperationRequest request, PrintWriter wr } } - private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) { + private void writeHttpMethodIfNecessary(OperationRequest request, + PrintWriter writer) { if (!HttpMethod.GET.equals(request.getMethod())) { writer.print(String.format(" -X %s", request.getMethod())); } @@ -145,8 +148,8 @@ private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) writer.printf("@%s", part.getSubmittedFileName()); } if (part.getHeaders().getContentType() != null) { - writer.append(";type=").append( - part.getHeaders().getContentType().toString()); + writer.append(";type=") + .append(part.getHeaders().getContentType().toString()); } writer.append("'"); @@ -170,7 +173,8 @@ else if (isPutOrPost(request)) { } } - private void writeContentUsingParameters(OperationRequest request, PrintWriter writer) { + private void writeContentUsingParameters(OperationRequest request, + PrintWriter writer) { Parameters uniqueParameters = getUniqueParameters(request); String queryString = uniqueParameters.toQueryString(); if (StringUtils.hasText(queryString)) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java index d09d6e441..0982adf95 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java @@ -64,8 +64,8 @@ private void processParameter(String parameter, Parameters parameters) { parameters.add(decode(name), decode(value)); } else { - throw new IllegalArgumentException("The parameter '" + parameter - + "' is malformed"); + throw new IllegalArgumentException( + "The parameter '" + parameter + "' is malformed"); } } @@ -74,8 +74,8 @@ private String decode(String encoded) { return URLDecoder.decode(encoded, "UTF-8"); } catch (UnsupportedEncodingException ex) { - throw new IllegalStateException("Unable to URL encode " + encoded - + " using UTF-8", ex); + throw new IllegalStateException( + "Unable to URL encode " + encoded + " using UTF-8", ex); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java index 85ee026af..7c07b91e0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java @@ -18,4 +18,3 @@ * Documenting the curl command required to make a request to a RESTful API. */ package org.springframework.restdocs.curl; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 26d209fab..38d1e35f6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -66,12 +66,9 @@ protected HttpRequestSnippet(Map attributes) { protected Map createModel(Operation operation) { Map model = new HashMap<>(); model.put("method", operation.getRequest().getMethod()); - model.put( - "path", - operation.getRequest().getUri().getRawPath() - + (StringUtils.hasText(operation.getRequest().getUri() - .getRawQuery()) ? "?" - + operation.getRequest().getUri().getRawQuery() : "")); + model.put("path", operation.getRequest().getUri().getRawPath() + + (StringUtils.hasText(operation.getRequest().getUri().getRawQuery()) + ? "?" + operation.getRequest().getUri().getRawQuery() : "")); model.put("headers", getHeaders(operation.getRequest())); model.put("requestBody", getRequestBody(operation.getRequest())); return model; @@ -149,8 +146,8 @@ private void writePartBoundary(PrintWriter writer) { } private void writePart(OperationRequestPart part, PrintWriter writer) { - writePart(part.getName(), part.getContentAsString(), part.getHeaders() - .getContentType(), writer); + writePart(part.getName(), part.getContentAsString(), + part.getHeaders().getContentType(), writer); } private void writePart(String name, String value, MediaType contentType, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java index 5d3206012..da609abbe 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/package-info.java @@ -19,4 +19,3 @@ * returned. */ package org.springframework.restdocs.http; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java index cc9f115ad..a345cd2de 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AbstractJsonLinkExtractor.java @@ -20,10 +20,10 @@ import java.util.List; import java.util.Map; -import org.springframework.restdocs.operation.OperationResponse; - import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.restdocs.operation.OperationResponse; + /** * Abstract base class for a {@link LinkExtractor} that extracts links from JSON. * @@ -37,8 +37,8 @@ abstract class AbstractJsonLinkExtractor implements LinkExtractor { @SuppressWarnings("unchecked") public Map> extractLinks(OperationResponse response) throws IOException { - Map jsonContent = this.objectMapper.readValue( - response.getContent(), Map.class); + Map jsonContent = this.objectMapper + .readValue(response.getContent(), Map.class); return extractLinks(jsonContent); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index a4fbf42d4..d5ddaf0b4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -170,7 +170,7 @@ public static LinkExtractor halLinks() { * "href": "http://example.com/foo" * } * ] - * } + * } * * * @return The extractor for Atom-style links diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java index eed9dfb4c..112886f0b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -88,8 +88,8 @@ public boolean equals(Object obj) { @Override public String toString() { - return new ToStringCreator(this).append("rel", this.rel) - .append("href", this.href).toString(); + return new ToStringCreator(this).append("rel", this.rel).append("href", this.href) + .toString(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index b7f868c85..771e2e7a8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -56,7 +56,8 @@ public class LinksSnippet extends TemplatedSnippet { * @param linkExtractor the link extractor * @param descriptors the link descriptors */ - protected LinksSnippet(LinkExtractor linkExtractor, List descriptors) { + protected LinksSnippet(LinkExtractor linkExtractor, + List descriptors) { this(linkExtractor, descriptors, null); } @@ -76,9 +77,10 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip for (LinkDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), "The descriptor for link '" - + descriptor.getRel() + "' must either have a description or be" - + " marked as " + "ignored"); + Assert.notNull(descriptor.getDescription(), + "The descriptor for link '" + descriptor.getRel() + + "' must either have a description or be" + " marked as " + + "ignored"); } this.descriptorsByRel.put(descriptor.getRel(), descriptor); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java index c8d3dab75..0d3ad26d2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/package-info.java @@ -18,4 +18,3 @@ * Documenting a RESTful API that uses hypermedia. */ package org.springframework.restdocs.hypermedia; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java index c5ede617d..41e8fec3e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -49,8 +49,8 @@ public HttpHeaders getHeaders() { public String getContentAsString() { if (this.content.length > 0) { Charset charset = extractCharsetFromContentTypeHeader(); - return charset != null ? new String(this.content, charset) : new String( - this.content); + return charset != null ? new String(this.content, charset) + : new String(this.content); } return ""; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 693be1ddb..c105fd75c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -45,8 +45,8 @@ public class OperationRequestFactory { public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, Collection parts) { - return new StandardOperationRequest(uri, method, content, augmentHeaders(headers, - uri, content), parameters, parts); + return new StandardOperationRequest(uri, method, content, + augmentHeaders(headers, uri, content), parameters, parts); } /** @@ -74,7 +74,8 @@ public OperationRequest createFrom(OperationRequest original, byte[] newContent) * * @return The new request with the new content */ - public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { + public OperationRequest createFrom(OperationRequest original, + HttpHeaders newHeaders) { return new StandardOperationRequest(original.getUri(), original.getMethod(), original.getContent(), newHeaders, original.getParameters(), original.getParts()); @@ -89,8 +90,8 @@ private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, byte[] updatedContent) { - return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent( - updatedContent).getHeaders(); + return new HttpHeadersHelper(originalHeaders) + .updateContentLengthHeaderIfPresent(updatedContent).getHeaders(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java index c3417c9f8..1cfd1434a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationResponseFactory.java @@ -36,7 +36,8 @@ public class OperationResponseFactory { * @param content the content of the request * @return the {@code OperationResponse} */ - public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] content) { + public OperationResponse create(HttpStatus status, HttpHeaders headers, + byte[] content) { return new StandardOperationResponse(status, augmentHeaders(headers, content), content); } @@ -53,8 +54,8 @@ public OperationResponse create(HttpStatus status, HttpHeaders headers, byte[] c * @return The new response with the new content */ public OperationResponse createFrom(OperationResponse original, byte[] newContent) { - return new StandardOperationResponse(original.getStatus(), getUpdatedHeaders( - original.getHeaders(), newContent), newContent); + return new StandardOperationResponse(original.getStatus(), + getUpdatedHeaders(original.getHeaders(), newContent), newContent); } /** @@ -66,7 +67,8 @@ public OperationResponse createFrom(OperationResponse original, byte[] newConten * * @return The new response with the new headers */ - public OperationResponse createFrom(OperationResponse original, HttpHeaders newHeaders) { + public OperationResponse createFrom(OperationResponse original, + HttpHeaders newHeaders) { return new StandardOperationResponse(original.getStatus(), newHeaders, original.getContent()); } @@ -78,8 +80,8 @@ private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, byte[] content) private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, byte[] updatedContent) { - return new HttpHeadersHelper(originalHeaders).updateContentLengthHeaderIfPresent( - updatedContent).getHeaders(); + return new HttpHeadersHelper(originalHeaders) + .updateContentLengthHeaderIfPresent(updatedContent).getHeaders(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index 305d7b2a7..a09bd645f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -28,8 +28,8 @@ * * @author Andy Wilkinson */ -class StandardOperationRequest extends AbstractOperationMessage implements - OperationRequest { +class StandardOperationRequest extends AbstractOperationMessage + implements OperationRequest { private HttpMethod method; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java index 8ea231a4d..e9fee9d75 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequestPart.java @@ -23,8 +23,8 @@ * * @author Andy Wilkinson */ -class StandardOperationRequestPart extends AbstractOperationMessage implements - OperationRequestPart { +class StandardOperationRequestPart extends AbstractOperationMessage + implements OperationRequestPart { private final String name; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java index 32dfa57f7..0131a4b5a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationResponse.java @@ -24,8 +24,8 @@ * * @author Andy Wilkinson */ -class StandardOperationResponse extends AbstractOperationMessage implements - OperationResponse { +class StandardOperationResponse extends AbstractOperationMessage + implements OperationResponse { private final HttpStatus status; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java index d7786bca2..e9ba93d90 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/package-info.java @@ -15,8 +15,7 @@ */ /** - * Operation API that describes a request that was sent and the response that was - * received when calling a RESTful API. + * Operation API that describes a request that was sent and the response that was received + * when calling a RESTful API. */ package org.springframework.restdocs.operation; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java index 60c704aa6..d18948e37 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessor.java @@ -54,8 +54,8 @@ public OperationRequest preprocess(OperationRequest request) { @Override public OperationResponse preprocess(OperationResponse response) { - byte[] modifiedContent = this.contentModifier.modifyContent( - response.getContent(), response.getHeaders().getContentType()); + byte[] modifiedContent = this.contentModifier.modifyContent(response.getContent(), + response.getHeaders().getContentType()); return this.responseFactory.createFrom(response, modifiedContent); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java index 9af9c03f3..928f700f3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifier.java @@ -29,8 +29,8 @@ class LinkMaskingContentModifier implements ContentModifier { private static final String DEFAULT_MASK = "..."; - private static final Pattern LINK_HREF = Pattern.compile( - "\"href\"\\s*:\\s*\"(.*?)\"", Pattern.DOTALL); + private static final Pattern LINK_HREF = Pattern.compile("\"href\"\\s*:\\s*\"(.*?)\"", + Pattern.DOTALL); private final ContentModifier contentModifier; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index b4aa590dd..35b5c1fd6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -90,7 +90,8 @@ public static OperationPreprocessor removeHeaders(String... headersToRemove) { * @return the preprocessor */ public static OperationPreprocessor maskLinks() { - return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier()); + return new ContentModifyingOperationPreprocessor( + new LinkMaskingContentModifier()); } /** @@ -101,8 +102,8 @@ public static OperationPreprocessor maskLinks() { * @return the preprocessor */ public static OperationPreprocessor maskLinks(String mask) { - return new ContentModifyingOperationPreprocessor(new LinkMaskingContentModifier( - mask)); + return new ContentModifyingOperationPreprocessor( + new LinkMaskingContentModifier(mask)); } /** @@ -114,7 +115,8 @@ public static OperationPreprocessor maskLinks(String mask) { * @param replacement the replacement * @return the preprocessor */ - public static OperationPreprocessor replacePattern(Pattern pattern, String replacement) { + public static OperationPreprocessor replacePattern(Pattern pattern, + String replacement) { return new ContentModifyingOperationPreprocessor( new PatternReplacingContentModifier(pattern, replacement)); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index 9665fd09c..b722038ec 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -34,15 +34,15 @@ import javax.xml.transform.sax.SAXSource; import javax.xml.transform.stream.StreamResult; -import org.springframework.http.MediaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.XMLReader; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; +import org.springframework.http.MediaType; /** * A {@link ContentModifier} that modifies the content by pretty printing it. @@ -52,8 +52,8 @@ public class PrettyPrintingContentModifier implements ContentModifier { private static final List PRETTY_PRINTERS = Collections - .unmodifiableList(Arrays.asList(new JsonPrettyPrinter(), - new XmlPrettyPrinter())); + .unmodifiableList( + Arrays.asList(new JsonPrettyPrinter(), new XmlPrettyPrinter())); @Override public byte[] modifyContent(byte[] originalContent, MediaType contentType) { @@ -98,8 +98,8 @@ private SAXSource createSaxSource(byte[] original) SAXParser parser = parserFactory.newSAXParser(); XMLReader xmlReader = parser.getXMLReader(); xmlReader.setErrorHandler(new SilentErrorHandler()); - return new SAXSource(xmlReader, new InputSource(new ByteArrayInputStream( - original))); + return new SAXSource(xmlReader, + new InputSource(new ByteArrayInputStream(original))); } private static final class SilentErrorListener implements ErrorListener { @@ -111,7 +111,8 @@ public void warning(TransformerException exception) } @Override - public void error(TransformerException exception) throws TransformerException { + public void error(TransformerException exception) + throws TransformerException { // Suppress } @@ -146,8 +147,8 @@ private static final class JsonPrettyPrinter implements PrettyPrinter { @Override public String prettyPrint(byte[] original) throws IOException { - ObjectMapper objectMapper = new ObjectMapper().configure( - SerializationFeature.INDENT_OUTPUT, true); + ObjectMapper objectMapper = new ObjectMapper() + .configure(SerializationFeature.INDENT_OUTPUT, true); return objectMapper.writeValueAsString(objectMapper.readTree(original)); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java index 55913531d..a00ceb0e5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/package-info.java @@ -18,4 +18,3 @@ * Support for preprocessing an operation prior to it being documented. */ package org.springframework.restdocs.operation.preprocess; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java index 2d3767b2d..73fee6695 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/package-info.java @@ -18,4 +18,3 @@ * Core Spring REST Docs classes. */ package org.springframework.restdocs; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 3257be17c..dcff13bdd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -57,9 +57,10 @@ protected AbstractFieldsSnippet(String type, List descriptors, for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), "The descriptor for field '" - + descriptor.getPath() + "' must either have a description or" - + " be marked as " + "ignored"); + Assert.notNull(descriptor.getDescription(), + "The descriptor for field '" + descriptor.getPath() + + "' must either have a description or" + " be marked as " + + "ignored"); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index b08f14279..779b75fbc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -44,13 +44,13 @@ class JsonContentHandler implements ContentHandler { } @Override - public List findMissingFields(List fieldDescriptors) { + public List findMissingFields( + List fieldDescriptors) { List missingFields = new ArrayList<>(); Object payload = readContent(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { - if (!fieldDescriptor.isOptional() - && !this.fieldProcessor.hasField( - JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) { + if (!fieldDescriptor.isOptional() && !this.fieldProcessor.hasField( + JsonFieldPath.compile(fieldDescriptor.getPath()), payload)) { missingFields.add(fieldDescriptor); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index 3eefe20c1..10478a839 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -87,8 +87,8 @@ private static List extractSegments(String path) { List segments = new ArrayList<>(); while (matcher.find()) { if (previous != matcher.start()) { - segments.addAll(extractDotSeparatedSegments(path.substring(previous, - matcher.start()))); + segments.addAll(extractDotSeparatedSegments( + path.substring(previous, matcher.start()))); } if (matcher.group(1) != null) { segments.add(matcher.group(1)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index 5e4ed29d8..fb63a2148 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -89,26 +89,30 @@ else if (context.getPayload() instanceof Map } } - private void handleListPayload(ProcessingContext context, MatchCallback matchCallback) { + private void handleListPayload(ProcessingContext context, + MatchCallback matchCallback) { List list = context.getPayload(); final Iterator items = list.iterator(); if (context.isLeaf()) { while (items.hasNext()) { Object item = items.next(); - matchCallback.foundMatch(new ListMatch(items, list, item, context - .getParentMatch())); + matchCallback.foundMatch( + new ListMatch(items, list, item, context.getParentMatch())); } } else { while (items.hasNext()) { Object item = items.next(); - traverse(context.descend(item, new ListMatch(items, list, item, - context.parent)), matchCallback); + traverse( + context.descend(item, + new ListMatch(items, list, item, context.parent)), + matchCallback); } } } - private void handleMapPayload(ProcessingContext context, MatchCallback matchCallback) { + private void handleMapPayload(ProcessingContext context, + MatchCallback matchCallback) { Map map = context.getPayload(); Object item = map.get(context.getSegment()); MapMatch mapMatch = new MapMatch(item, map, context.getSegment(), @@ -238,8 +242,8 @@ private Match getParentMatch() { } private ProcessingContext descend(Object payload, Match match) { - return new ProcessingContext(payload, this.path, this.segments.subList(1, - this.segments.size()), match); + return new ProcessingContext(payload, this.path, + this.segments.subList(1, this.segments.size()), match); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 798013736..67e2763d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -63,7 +63,8 @@ class XmlContentHandler implements ContentHandler { } @Override - public List findMissingFields(List fieldDescriptors) { + public List findMissingFields( + List fieldDescriptors) { List missingFields = new ArrayList<>(); Document payload = readPayload(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { @@ -78,7 +79,8 @@ public List findMissingFields(List fieldDescri return missingFields; } - private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document payload) { + private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, + Document payload) { try { return (NodeList) createXPath(fieldDescriptor.getPath()).evaluate(payload, XPathConstants.NODESET); @@ -90,15 +92,16 @@ private NodeList findMatchingNodes(FieldDescriptor fieldDescriptor, Document pay private Document readPayload() { try { - return this.documentBuilder.parse(new InputSource(new ByteArrayInputStream( - this.rawContent))); + return this.documentBuilder + .parse(new InputSource(new ByteArrayInputStream(this.rawContent))); } catch (Exception ex) { throw new PayloadHandlingException(ex); } } - private XPathExpression createXPath(String fieldPath) throws XPathExpressionException { + private XPathExpression createXPath(String fieldPath) + throws XPathExpressionException { return XPathFactory.newInstance().newXPath().compile(fieldPath); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java index f306a0191..2c3281d6c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/package-info.java @@ -18,4 +18,3 @@ * Documenting the payload of a RESTful API's requests and responses. */ package org.springframework.restdocs.payload; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 7eb6e11c1..0be930228 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -54,7 +54,8 @@ protected AbstractParametersSnippet(String snippetName, List descriptors, Map attributes) { super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { - Assert.notNull(descriptor.getName(), "Parameter descriptors must have a name"); + Assert.notNull(descriptor.getName(), + "Parameter descriptors must have a name"); if (!descriptor.isIgnored()) { Assert.notNull(descriptor.getDescription(), "The descriptor for parameter '" + descriptor.getName() @@ -71,7 +72,8 @@ protected Map createModel(Operation operation) { Map model = new HashMap<>(); List> parameters = new ArrayList<>(); - for (Entry entry : this.descriptorsByName.entrySet()) { + for (Entry entry : this.descriptorsByName + .entrySet()) { ParameterDescriptor descriptor = entry.getValue(); if (!descriptor.isIgnored()) { parameters.add(createModelForDescriptor(descriptor)); @@ -131,7 +133,8 @@ protected final Map getFieldDescriptors() { * @param descriptor the descriptor * @return the model */ - protected Map createModelForDescriptor(ParameterDescriptor descriptor) { + protected Map createModelForDescriptor( + ParameterDescriptor descriptor) { Map model = new HashMap<>(); model.put("name", descriptor.getName()); model.put("description", descriptor.getDescription()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 5df99f6f6..4186faa08 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -90,8 +90,8 @@ protected Set extractActualParameters(Operation operation) { } private String extractUrlTemplate(Operation operation) { - String urlTemplate = (String) operation.getAttributes().get( - "org.springframework.restdocs.urlTemplate"); + String urlTemplate = (String) operation.getAttributes() + .get("org.springframework.restdocs.urlTemplate"); Assert.notNull(urlTemplate, "urlTemplate not found. Did you use RestDocumentationRequestBuilders to " + "build the request?"); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java index 744aa7f0e..c2828b7b5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/package-info.java @@ -18,4 +18,3 @@ * Documenting query and path parameters of requests sent to a RESTful API. */ package org.springframework.restdocs.request; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java index 8174c3815..477431ee5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/IgnorableDescriptor.java @@ -22,8 +22,8 @@ * @param the type of the descriptor * @author Andy Wilkinson */ -public abstract class IgnorableDescriptor> extends - AbstractDescriptor { +public abstract class IgnorableDescriptor> + extends AbstractDescriptor { private boolean ignored = false; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java index 6c3e00be2..81898c250 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolver.java @@ -137,8 +137,9 @@ private String camelCaseToSeparator(String string, String separator) { Matcher matcher = CAMEL_CASE_PATTERN.matcher(string); StringBuffer result = new StringBuffer(); while (matcher.find()) { - String replacement = (matcher.start() > 0) ? separator - + matcher.group(1).toLowerCase() : matcher.group(1).toLowerCase(); + String replacement = (matcher.start() > 0) + ? separator + matcher.group(1).toLowerCase() + : matcher.group(1).toLowerCase(); matcher.appendReplacement(result, replacement); } matcher.appendTail(result); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 1ef51ea4e..883e1c115 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -59,7 +59,8 @@ public Writer resolve(String operationName, String snippetName, if (outputFile != null) { createDirectoriesIfNecessary(outputFile); - return new OutputStreamWriter(new FileOutputStream(outputFile), this.encoding); + return new OutputStreamWriter(new FileOutputStream(outputFile), + this.encoding); } else { return new OutputStreamWriter(System.out, this.encoding); @@ -92,7 +93,8 @@ private File makeRelativeToConfiguredOutputDir(File outputFile, private void createDirectoriesIfNecessary(File outputFile) { File parent = outputFile.getParentFile(); if (!parent.isDirectory() && !parent.mkdirs()) { - throw new IllegalStateException("Failed to create directory '" + parent + "'"); + throw new IllegalStateException( + "Failed to create directory '" + parent + "'"); } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index 015d1fe8e..dedfa98a3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -57,10 +57,10 @@ protected TemplatedSnippet(String snippetName, Map attributes) { public void document(Operation operation) throws IOException { RestDocumentationContext context = (RestDocumentationContext) operation .getAttributes().get(RestDocumentationContext.class.getName()); - WriterResolver writerResolver = (WriterResolver) operation.getAttributes().get( - WriterResolver.class.getName()); - try (Writer writer = writerResolver.resolve(operation.getName(), - this.snippetName, context)) { + WriterResolver writerResolver = (WriterResolver) operation.getAttributes() + .get(WriterResolver.class.getName()); + try (Writer writer = writerResolver.resolve(operation.getName(), this.snippetName, + context)) { Map model = createModel(operation); model.putAll(this.attributes); TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes() diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java index 9c16a3e25..90a6642b1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/package-info.java @@ -18,4 +18,3 @@ * Snippet generation. */ package org.springframework.restdocs.snippet; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java index 4e3d38e30..847f6ee85 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java @@ -38,10 +38,11 @@ public Resource resolveTemplateResource(String name) { "org/springframework/restdocs/templates/" + name + ".snippet"); if (!classPathResource.exists()) { classPathResource = new ClassPathResource( - "org/springframework/restdocs/templates/default-" + name + ".snippet"); + "org/springframework/restdocs/templates/default-" + name + + ".snippet"); if (!classPathResource.exists()) { - throw new IllegalStateException("Template named '" + name - + "' could not be resolved"); + throw new IllegalStateException( + "Template named '" + name + "' could not be resolved"); } } return classPathResource; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index df142045a..3a5acc721 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -54,8 +54,8 @@ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) public Template compileTemplate(String name) throws IOException { Resource templateResource = this.templateResourceResolver .resolveTemplateResource(name); - return new MustacheTemplate(this.compiler.compile(new InputStreamReader( - templateResource.getInputStream()))); + return new MustacheTemplate(this.compiler + .compile(new InputStreamReader(templateResource.getInputStream()))); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java index 244b8d9e5..9f7231ce9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/package-info.java @@ -18,4 +18,3 @@ * JMustache-based implementation of the template API. */ package org.springframework.restdocs.templates.mustache; - diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java index cac448dd7..a0a11105a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/package-info.java @@ -18,4 +18,3 @@ * Template API used to render documentation snippets. */ package org.springframework.restdocs.templates; - diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index 6968fccff..a3dbd9abe 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -129,8 +129,7 @@ public void size() { @Test public void sizeList() { - assertThat( - this.constraintDescriptions.descriptionsForProperty("sizeList"), + assertThat(this.constraintDescriptions.descriptionsForProperty("sizeList"), contains("Size must be between 1 and 4 inclusive", "Size must be between 8 and 10 inclusive")); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index 8f2fa07d3..06334f51e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -70,8 +70,8 @@ public void defaultMessageAssertTrue() { public void defaultMessageDecimalMax() { Map configuration = new HashMap<>(); configuration.put("value", "9.875"); - String description = this.resolver.resolveDescription(new Constraint( - DecimalMax.class.getName(), configuration)); + String description = this.resolver.resolveDescription( + new Constraint(DecimalMax.class.getName(), configuration)); assertThat(description, is(equalTo("Must be at most 9.875"))); } @@ -79,8 +79,8 @@ public void defaultMessageDecimalMax() { public void defaultMessageDecimalMin() { Map configuration = new HashMap<>(); configuration.put("value", "1.5"); - String description = this.resolver.resolveDescription(new Constraint( - DecimalMin.class.getName(), configuration)); + String description = this.resolver.resolveDescription( + new Constraint(DecimalMin.class.getName(), configuration)); assertThat(description, is(equalTo("Must be at least 1.5"))); } @@ -89,16 +89,16 @@ public void defaultMessageDigits() { Map configuration = new HashMap<>(); configuration.put("integer", "2"); configuration.put("fraction", "5"); - String description = this.resolver.resolveDescription(new Constraint(Digits.class - .getName(), configuration)); - assertThat(description, is(equalTo("Must have at most 2 integral digits and 5 " - + "fractional digits"))); + String description = this.resolver.resolveDescription( + new Constraint(Digits.class.getName(), configuration)); + assertThat(description, is(equalTo( + "Must have at most 2 integral digits and 5 " + "fractional digits"))); } @Test public void defaultMessageFuture() { - String description = this.resolver.resolveDescription(new Constraint(Future.class - .getName(), Collections.emptyMap())); + String description = this.resolver.resolveDescription(new Constraint( + Future.class.getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be in the future"))); } @@ -106,8 +106,8 @@ public void defaultMessageFuture() { public void defaultMessageMax() { Map configuration = new HashMap<>(); configuration.put("value", 10); - String description = this.resolver.resolveDescription(new Constraint(Max.class - .getName(), configuration)); + String description = this.resolver + .resolveDescription(new Constraint(Max.class.getName(), configuration)); assertThat(description, is(equalTo("Must be at most 10"))); } @@ -115,8 +115,8 @@ public void defaultMessageMax() { public void defaultMessageMin() { Map configuration = new HashMap<>(); configuration.put("value", 10); - String description = this.resolver.resolveDescription(new Constraint(Min.class - .getName(), configuration)); + String description = this.resolver + .resolveDescription(new Constraint(Min.class.getName(), configuration)); assertThat(description, is(equalTo("Must be at least 10"))); } @@ -129,15 +129,15 @@ public void defaultMessageNotNull() { @Test public void defaultMessageNull() { - String description = this.resolver.resolveDescription(new Constraint(Null.class - .getName(), Collections.emptyMap())); + String description = this.resolver.resolveDescription(new Constraint( + Null.class.getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be null"))); } @Test public void defaultMessagePast() { - String description = this.resolver.resolveDescription(new Constraint(Past.class - .getName(), Collections.emptyMap())); + String description = this.resolver.resolveDescription(new Constraint( + Past.class.getName(), Collections.emptyMap())); assertThat(description, is(equalTo("Must be in the past"))); } @@ -145,8 +145,8 @@ public void defaultMessagePast() { public void defaultMessagePattern() { Map configuration = new HashMap<>(); configuration.put("regexp", "[A-Z][a-z]+"); - String description = this.resolver.resolveDescription(new Constraint( - Pattern.class.getName(), configuration)); + String description = this.resolver.resolveDescription( + new Constraint(Pattern.class.getName(), configuration)); assertThat(description, is(equalTo("Must match the regular expression '[A-Z][a-z]+'"))); } @@ -156,8 +156,8 @@ public void defaultMessageSize() { Map configuration = new HashMap<>(); configuration.put("min", 2); configuration.put("max", 10); - String description = this.resolver.resolveDescription(new Constraint(Size.class - .getName(), configuration)); + String description = this.resolver + .resolveDescription(new Constraint(Size.class.getName(), configuration)); assertThat(description, is(equalTo("Size must be between 2 and 10 inclusive"))); } @@ -167,9 +167,10 @@ public void customMessage() { @Override public URL getResource(String name) { - if (name.startsWith("org/springframework/restdocs/constraints/ConstraintDescriptions")) { - return super - .getResource("org/springframework/restdocs/constraints/TestConstraintDescriptions.properties"); + if (name.startsWith( + "org/springframework/restdocs/constraints/ConstraintDescriptions")) { + return super.getResource( + "org/springframework/restdocs/constraints/TestConstraintDescriptions.properties"); } return super.getResource(name); } @@ -194,14 +195,14 @@ public void customResourceBundle() { @Override protected Object[][] getContents() { - return new String[][] { { NotNull.class.getName() + ".description", - "Not null" } }; + return new String[][] { + { NotNull.class.getName() + ".description", "Not null" } }; } }; String description = new ResourceBundleConstraintDescriptionResolver(bundle) - .resolveDescription(new Constraint(NotNull.class.getName(), Collections - .emptyMap())); + .resolveDescription(new Constraint(NotNull.class.getName(), + Collections.emptyMap())); assertThat(description, is(equalTo("Not null"))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java index 89bce77b8..4ff83c65c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ValidatorConstraintResolverTests.java @@ -66,10 +66,8 @@ public void multipleFieldConstraints() { List constraints = this.resolver.resolveForProperty("multiple", ConstrainedFields.class); assertThat(constraints, hasSize(2)); - assertThat( - constraints, - containsInAnyOrder(constraint(NotNull.class), constraint(Size.class) - .config("min", 8).config("max", 16))); + assertThat(constraints, containsInAnyOrder(constraint(NotNull.class), + constraint(Size.class).config("min", 8).config("max", 16))); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 21c1973d6..c614cb838 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -21,6 +21,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -59,101 +60,98 @@ public class CurlRequestSnippetTests { public void getRequest() throws IOException { this.snippet.expectCurlRequest("get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - new CurlRequestSnippet().document(new OperationBuilder("get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .build()); + new CurlRequestSnippet().document( + new OperationBuilder("get-request", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").build()); } @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - new CurlRequestSnippet().document(new OperationBuilder("non-get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .method("POST").build()); + new CurlRequestSnippet().document( + new OperationBuilder("non-get-request", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").method("POST").build()); } @Test public void requestWithContent() throws IOException { - this.snippet.expectCurlRequest("request-with-content").withContents( - codeBlock("bash") + this.snippet.expectCurlRequest("request-with-content") + .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); new CurlRequestSnippet().document(new OperationBuilder("request-with-content", this.snippet.getOutputDirectory()).request("http://localhost/foo") - .content("content").build()); + .content("content").build()); } @Test public void getRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("request-with-query-string") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document(new OperationBuilder( - "request-with-query-string", this.snippet.getOutputDirectory()).request( - "http://localhost/foo?param=value").build()); + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i")); + new CurlRequestSnippet() + .document(new OperationBuilder("request-with-query-string", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param=value").build()); } @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest("post-request-with-query-string").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo?param=value' -i -X POST")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-query-string", this.snippet.getOutputDirectory()) - .request("http://localhost/foo?param=value").method("POST").build()); + this.snippet.expectCurlRequest("post-request-with-query-string") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); + new CurlRequestSnippet() + .document(new OperationBuilder("post-request-with-query-string", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param=value") + .method("POST").build()); } @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); + this.snippet.expectCurlRequest("post-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); new CurlRequestSnippet() .document(new OperationBuilder("post-request-with-one-parameter", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST").param("k1", "v1") - .build()); + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("k1", "v1").build()); } @Test public void postRequestWithMultipleParameters() throws IOException { this.snippet.expectCurlRequest("post-request-with-multiple-parameters") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X POST" - + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-multiple-parameters", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1", "v1", "v1-bis").param("k2", "v2").build()); + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST" + + " -d 'k1=v1&k1=v1-bis&k2=v2'")); + new CurlRequestSnippet() + .document(new OperationBuilder("post-request-with-multiple-parameters", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("k1", "v1", "v1-bis") + .param("k2", "v2").build()); } @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet - .expectCurlRequest("post-request-with-url-encoded-parameter") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-url-encoded-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1", "a&b").build()); + this.snippet.expectCurlRequest("post-request-with-url-encoded-parameter") + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); + new CurlRequestSnippet() + .document(new OperationBuilder("post-request-with-url-encoded-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("k1", "a&b").build()); } @Test public void postRequestWithQueryStringAndParameter() throws IOException { - this.snippet - .expectCurlRequest("post-request-with-query-string-and-parameter") - .withContents( - codeBlock("bash") - .content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-query-string-and-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo?a=alpha") - .method("POST").param("b", "bravo").build()); + this.snippet.expectCurlRequest("post-request-with-query-string-and-parameter") + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document( + new OperationBuilder("post-request-with-query-string-and-parameter", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?a=alpha").method("POST") + .param("b", "bravo").build()); } @Test @@ -161,62 +159,60 @@ public void postRequestWithOverlappingQueryStringAndParameters() throws IOExcept this.snippet .expectCurlRequest( "post-request-with-overlapping-query-string-and-parameters") - .withContents( - codeBlock("bash") - .content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); new CurlRequestSnippet().document(new OperationBuilder( - "post-request-with-overlapping-query-string-and-parameters", this.snippet - .getOutputDirectory()).request("http://localhost/foo?a=alpha") - .method("POST").param("a", "alpha").param("b", "bravo").build()); + "post-request-with-overlapping-query-string-and-parameters", + this.snippet.getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-one-parameter", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); + this.snippet.expectCurlRequest("put-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); + new CurlRequestSnippet() + .document(new OperationBuilder("put-request-with-one-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").param("k1", "v1").build()); } @Test public void putRequestWithMultipleParameters() throws IOException { this.snippet.expectCurlRequest("put-request-with-multiple-parameters") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X PUT" - + " -d 'k1=v1&k1=v1-bis&k2=v2'")); + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT" + + " -d 'k1=v1&k1=v1-bis&k2=v2'")); new CurlRequestSnippet() .document(new OperationBuilder("put-request-with-multiple-parameters", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").param("k1", "v1").param("k1", "v1-bis") + .param("k2", "v2").build()); } @Test public void putRequestWithUrlEncodedParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-url-encoded-parameter") - .withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); - new CurlRequestSnippet().document(new OperationBuilder( - "put-request-with-url-encoded-parameter", this.snippet - .getOutputDirectory()).request("http://localhost/foo") - .method("PUT").param("k1", "a&b").build()); + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); + new CurlRequestSnippet() + .document(new OperationBuilder("put-request-with-url-encoded-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").param("k1", "a&b").build()); } @Test public void requestWithHeaders() throws IOException { - this.snippet.expectCurlRequest("request-with-headers").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i" - + " -H 'Content-Type: application/json' -H 'a: alpha'")); + this.snippet.expectCurlRequest("request-with-headers") + .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + + " -H 'Content-Type: application/json' -H 'a: alpha'")); new CurlRequestSnippet().document(new OperationBuilder("request-with-headers", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .header("a", "alpha").build()); + this.snippet.getOutputDirectory()) + .request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); } @Test @@ -226,11 +222,14 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { + "'metadata={\"description\": \"foo\"}'"; this.snippet.expectCurlRequest("multipart-post-no-original-filename") .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-no-original-filename", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); + new CurlRequestSnippet() + .document(new OperationBuilder("multipart-post-no-original-filename", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); } @Test @@ -238,15 +237,17 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png;type=image/png'"; - this.snippet.expectCurlRequest("multipart-post-with-content-type").withContents( - codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); + this.snippet.expectCurlRequest("multipart-post-with-content-type") + .withContents(codeBlock("bash").content(expectedContent)); + new CurlRequestSnippet() + .document(new OperationBuilder("multipart-post-with-content-type", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -254,14 +255,15 @@ public void multipartPost() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png'"; - this.snippet.expectCurlRequest("multipart-post").withContents( - codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder("multipart-post", - this.snippet.getOutputDirectory()).request("http://localhost/upload") - .method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").build()); + this.snippet.expectCurlRequest("multipart-post") + .withContents(codeBlock("bash").content(expectedContent)); + new CurlRequestSnippet().document( + new OperationBuilder("multipart-post", this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -270,45 +272,46 @@ public void multipartPostWithParameters() throws IOException { + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " + "-F 'b=banana'"; - this.snippet.expectCurlRequest("multipart-post-with-parameters").withContents( - codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + this.snippet.expectCurlRequest("multipart-post-with-parameters") + .withContents(codeBlock("bash").content(expectedContent)); + new CurlRequestSnippet() + .document(new OperationBuilder("multipart-post-with-parameters", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); } @Test public void customAttributes() throws IOException { - this.snippet.expectCurlRequest("custom-attributes").withContents( - containsString("curl request title")); + this.snippet.expectCurlRequest("custom-attributes") + .withContents(containsString("curl request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("curl-request")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/curl-request-with-title.snippet")); new CurlRequestSnippet(attributes(key("title").value("curl request title"))) - .document(new OperationBuilder("custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost/foo").build()); + .document(new OperationBuilder("custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost/foo").build()); } @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { - this.snippet.expectCurlRequest("basic-auth").withContents( - codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -u 'user:secret'")); - new CurlRequestSnippet().document(new OperationBuilder("basic-auth", this.snippet - .getOutputDirectory()) - .request("http://localhost/foo") - .header(HttpHeaders.AUTHORIZATION, - "Basic " + Base64Utils.encodeToString("user:secret".getBytes())) - .build()); + this.snippet.expectCurlRequest("basic-auth").withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -u 'user:secret'")); + new CurlRequestSnippet().document( + new OperationBuilder("basic-auth", this.snippet.getOutputDirectory()) + .request("http://localhost/foo") + .header(HttpHeaders.AUTHORIZATION, + "Basic " + Base64Utils + .encodeToString("user:secret".getBytes())) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java index bfdc70107..634fede11 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java @@ -22,6 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.restdocs.operation.Parameters; import static org.hamcrest.CoreMatchers.equalTo; @@ -43,23 +44,23 @@ public class QueryStringParserTests { @Test public void noParameters() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost")); assertThat(parameters.size(), is(equalTo(0))); } @Test public void singleParameter() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost?a=alpha")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost?a=alpha")); assertThat(parameters.size(), is(equalTo(1))); assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); } @Test public void multipleParameters() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost?a=alpha&b=bravo&c=charlie")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost?a=alpha&b=bravo&c=charlie")); assertThat(parameters.size(), is(equalTo(3))); assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); assertThat(parameters, hasEntry("b", Arrays.asList("bravo"))); @@ -68,16 +69,16 @@ public void multipleParameters() { @Test public void multipleParametersWithSameKey() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost?a=apple&a=avocado")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost?a=apple&a=avocado")); assertThat(parameters.size(), is(equalTo(1))); assertThat(parameters, hasEntry("a", Arrays.asList("apple", "avocado"))); } @Test public void encoded() { - Parameters parameters = this.queryStringParser.parse(URI - .create("http://localhost?a=al%26%3Dpha")); + Parameters parameters = this.queryStringParser + .parse(URI.create("http://localhost?a=al%26%3Dpha")); assertThat(parameters.size(), is(equalTo(1))); assertThat(parameters, hasEntry("a", Arrays.asList("al&=pha"))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index b5b2d1e4c..279371693 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -22,6 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; @@ -56,44 +57,51 @@ public class RequestHeadersSnippetTests { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectRequestHeaders("request-with-headers").withContents( - tableWithHeader("Name", "Description").row("X-Test", "one") + this.snippet.expectRequestHeaders("request-with-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") .row("Accept", "two").row("Accept-Encoding", "three") .row("Accept-Language", "four").row("Cache-Control", "five") .row("Connection", "six")); - new RequestHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one"), headerWithName("Accept") - .description("two"), headerWithName("Accept-Encoding") - .description("three"), headerWithName("Accept-Language") - .description("four"), headerWithName("Cache-Control") - .description("five"), - headerWithName("Connection").description("six"))) - .document(new OperationBuilder("request-with-headers", this.snippet - .getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .header("Accept-Encoding", "gzip, deflate") - .header("Accept-Language", "en-US,en;q=0.5") - .header("Cache-Control", "max-age=0") - .header("Connection", "keep-alive").build()); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"), + headerWithName("Accept").description("two"), + headerWithName("Accept-Encoding").description("three"), + headerWithName("Accept-Language").description("four"), + headerWithName("Cache-Control").description("five"), + headerWithName("Connection").description("six"))) + .document(new OperationBuilder("request-with-headers", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .header("X-Test", "test") + .header("Accept", "*/*") + .header("Accept-Encoding", + "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive").build()); } @Test public void caseInsensitiveRequestHeaders() throws IOException { - this.snippet - .expectRequestHeaders("case-insensitive-request-headers") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder( - "case-insensitive-request-headers", this.snippet.getOutputDirectory()) - .request("/").header("X-test", "test").build()); + this.snippet.expectRequestHeaders("case-insensitive-request-headers") + .withContents( + tableWithHeader("Name", "Description").row("X-Test", "one")); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"))) + .document(new OperationBuilder("case-insensitive-request-headers", + this.snippet.getOutputDirectory()).request("/") + .header("X-test", "test").build()); } @Test public void undocumentedRequestHeader() throws IOException { - new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder("undocumented-request-header", - this.snippet.getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*").build()); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"))) + .document(new OperationBuilder("undocumented-request-header", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .build()); } @Test @@ -102,9 +110,11 @@ public void missingRequestHeader() throws IOException { this.thrown .expectMessage(equalTo("Headers with the following names were not found" + " in the request: [Accept]")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( - "one"))).document(new OperationBuilder("missing-request-headers", - this.snippet.getOutputDirectory()).request("http://localhost").build()); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("Accept").description("one"))) + .document(new OperationBuilder("missing-request-headers", + this.snippet.getOutputDirectory()) + .request("http://localhost").build()); } @Test @@ -113,11 +123,13 @@ public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOExceptio this.thrown .expectMessage(endsWith("Headers with the following names were not found" + " in the request: [Accept]")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("Accept").description( - "one"))).document(new OperationBuilder( - "undocumented-request-header-and-missing-request-header", this.snippet - .getOutputDirectory()).request("http://localhost") - .header("X-Test", "test").build()); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("Accept").description("one"))) + .document(new OperationBuilder( + "undocumented-request-header-and-missing-request-header", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .header("X-Test", "test").build()); } @Test @@ -129,21 +141,26 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { .row("Accept-Encoding", "two", "bravo") .row("Accept", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")).willReturn( - snippetResource("request-headers-with-extra-column")); + given(resolver.resolveTemplateResource("request-headers")) + .willReturn(snippetResource("request-headers-with-extra-column")); new RequestHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one").attributes( - key("foo").value("alpha")), - headerWithName("Accept-Encoding").description("two").attributes( - key("foo").value("bravo")), - headerWithName("Accept").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "request-headers-with-custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).request("http://localhost") - .header("X-Test", "test").header("Accept-Encoding", "gzip, deflate") - .header("Accept", "*/*").build()); + headerWithName("X-Test").description("one") + .attributes(key("foo").value("alpha")), + headerWithName("Accept-Encoding").description("two") + .attributes(key("foo").value("bravo")), + headerWithName("Accept").description("three") + .attributes(key("foo").value("charlie")))) + .document(new OperationBuilder( + "request-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .header("X-Test", "test") + .header("Accept-Encoding", + "gzip, deflate") + .header("Accept", "*/*").build()); } @Test @@ -151,20 +168,22 @@ public void requestHeadersWithCustomAttributes() throws IOException { this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") .withContents(startsWith(".Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-headers")).willReturn( - snippetResource("request-headers-with-title")); - new RequestHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one")), attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("request-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").header("X-Test", "test").build()); + given(resolver.resolveTemplateResource("request-headers")) + .willReturn(snippetResource("request-headers-with-title")); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one")), + attributes(key("title").value("Custom title"))).document( + new OperationBuilder("request-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost") + .header("X-Test", "test").build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 24487094f..8f5dd609f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -22,6 +22,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; @@ -56,58 +57,65 @@ public class ResponseHeadersSnippetTests { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders("response-headers").withContents( - tableWithHeader("Name", "Description").row("X-Test", "one") + this.snippet.expectResponseHeaders("response-headers") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") .row("Content-Type", "two").row("Etag", "three") .row("Cache-Control", "five").row("Vary", "six")); - new ResponseHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one"), - headerWithName("Content-Type").description("two"), headerWithName("Etag") - .description("three"), headerWithName("Cache-Control") - .description("five"), headerWithName("Vary").description("six"))) - .document(new OperationBuilder("response-headers", this.snippet - .getOutputDirectory()).response().header("X-Test", "test") - .header("Content-Type", "application/json") - .header("Etag", "lskjadldj3ii32l2ij23") - .header("Cache-Control", "max-age=0") - .header("Vary", "User-Agent").build()); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"), + headerWithName("Content-Type").description("two"), + headerWithName("Etag").description("three"), + headerWithName("Cache-Control").description("five"), + headerWithName("Vary").description("six"))) + .document(new OperationBuilder("response-headers", + this.snippet.getOutputDirectory()).response() + .header("X-Test", "test") + .header("Content-Type", + "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0") + .header("Vary", "User-Agent").build()); } @Test public void caseInsensitiveResponseHeaders() throws IOException { - this.snippet - .expectResponseHeaders("case-insensitive-response-headers") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder( - "case-insensitive-response-headers", this.snippet.getOutputDirectory()) - .response().header("X-test", "test").build()); + this.snippet.expectResponseHeaders("case-insensitive-response-headers") + .withContents( + tableWithHeader("Name", "Description").row("X-Test", "one")); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"))).document( + new OperationBuilder("case-insensitive-response-headers", + this.snippet.getOutputDirectory()).response() + .header("X-test", "test").build()); } @Test public void responseHeadersWithCustomDescriptorAttributes() throws IOException { this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") - .withContents( - tableWithHeader("Name", "Description", "Foo") - .row("X-Test", "one", "alpha") - .row("Content-Type", "two", "bravo") - .row("Etag", "three", "charlie")); + .withContents(tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo") + .row("Etag", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")).willReturn( - snippetResource("response-headers-with-extra-column")); + given(resolver.resolveTemplateResource("response-headers")) + .willReturn(snippetResource("response-headers-with-extra-column")); new ResponseHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one").attributes( - key("foo").value("alpha")), - headerWithName("Content-Type").description("two").attributes( - key("foo").value("bravo")), - headerWithName("Etag").description("three").attributes( - key("foo").value("charlie")))).document(new OperationBuilder( - "response-headers-with-custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .header("X-Test", "test").header("Content-Type", "application/json") - .header("Etag", "lskjadldj3ii32l2ij23").build()); + headerWithName("X-Test").description("one") + .attributes(key("foo").value("alpha")), + headerWithName("Content-Type").description("two") + .attributes(key("foo").value("bravo")), + headerWithName("Etag").description("three") + .attributes(key("foo").value("charlie")))) + .document(new OperationBuilder( + "response-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response().header("X-Test", "test") + .header("Content-Type", + "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .build()); } @Test @@ -115,23 +123,26 @@ public void responseHeadersWithCustomAttributes() throws IOException { this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") .withContents(startsWith(".Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-headers")).willReturn( - snippetResource("response-headers-with-title")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one")), attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("response-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .header("X-Test", "test").build()); + given(resolver.resolveTemplateResource("response-headers")) + .willReturn(snippetResource("response-headers-with-title")); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one")), + attributes(key("title").value("Custom title"))).document( + new OperationBuilder("response-headers-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .response().header("X-Test", "test").build()); } @Test public void undocumentedResponseHeader() throws IOException { - new ResponseHeadersSnippet(Arrays.asList(headerWithName("X-Test").description( - "one"))).document(new OperationBuilder("undocumented-response-header", - this.snippet.getOutputDirectory()).response().header("X-Test", "test") - .header("Content-Type", "*/*").build()); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("X-Test").description("one"))) + .document(new OperationBuilder("undocumented-response-header", + this.snippet.getOutputDirectory()).response() + .header("X-Test", "test") + .header("Content-Type", "*/*").build()); } @Test @@ -140,10 +151,10 @@ public void missingResponseHeader() throws IOException { this.thrown .expectMessage(equalTo("Headers with the following names were not found" + " in the response: [Content-Type]")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") - .description("one"))).document(new OperationBuilder( - "missing-response-headers", this.snippet.getOutputDirectory()).response() - .build()); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("Content-Type").description("one"))) + .document(new OperationBuilder("missing-response-headers", + this.snippet.getOutputDirectory()).response().build()); } @Test @@ -152,16 +163,17 @@ public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOExcept this.thrown .expectMessage(endsWith("Headers with the following names were not found" + " in the response: [Content-Type]")); - new ResponseHeadersSnippet(Arrays.asList(headerWithName("Content-Type") - .description("one"))).document(new OperationBuilder( - "undocumented-response-header-and-missing-response-header", this.snippet - .getOutputDirectory()).response().header("X-Test", "test") - .build()); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("Content-Type").description("one"))) + .document(new OperationBuilder( + "undocumented-response-header-and-missing-response-header", + this.snippet.getOutputDirectory()).response() + .header("X-Test", "test").build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index f948fb103..a2975674a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -20,6 +20,7 @@ import org.junit.Rule; import org.junit.Test; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -53,181 +54,200 @@ public class HttpRequestSnippetTests { @Test public void getRequest() throws IOException { - this.snippet.expectHttpRequest("get-request").withContents( - httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a").header( - HttpHeaders.HOST, "localhost")); + this.snippet.expectHttpRequest("get-request") + .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") + .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(new OperationBuilder("get-request", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .header("Alpha", "a").build()); + new HttpRequestSnippet().document( + new OperationBuilder("get-request", this.snippet.getOutputDirectory()) + .request("http://localhost/foo").header("Alpha", "a").build()); } @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpRequest("get-request-with-query-string").withContents( - httpRequest(RequestMethod.GET, "/foo?bar=baz").header(HttpHeaders.HOST, - "localhost")); + this.snippet.expectHttpRequest("get-request-with-query-string") + .withContents(httpRequest(RequestMethod.GET, "/foo?bar=baz") + .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(new OperationBuilder( - "get-request-with-query-string", this.snippet.getOutputDirectory()) - .request("http://localhost/foo?bar=baz").build()); + new HttpRequestSnippet() + .document(new OperationBuilder("get-request-with-query-string", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?bar=baz").build()); } @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("post-request-with-content").withContents( - httpRequest(RequestMethod.POST, "/foo") + this.snippet.expectHttpRequest("post-request-with-content") + .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost").content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-content", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST").content(content).build()); + new HttpRequestSnippet() + .document(new OperationBuilder("post-request-with-content", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").content(content).build()); } @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpRequest("post-request-with-charset").withContents( - httpRequest(RequestMethod.POST, "/foo") + this.snippet.expectHttpRequest("post-request-with-charset") + .withContents(httpRequest(RequestMethod.POST, "/foo") .header("Content-Type", "text/plain;charset=UTF-8") .header(HttpHeaders.HOST, "localhost") .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-charset", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST") - .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) - .build()); + new HttpRequestSnippet() + .document(new OperationBuilder("post-request-with-charset", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST") + .header("Content-Type", "text/plain;charset=UTF-8") + .content(contentBytes).build()); } @Test public void postRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest("post-request-with-parameter").withContents( - httpRequest(RequestMethod.POST, "/foo") + this.snippet.expectHttpRequest("post-request-with-parameter") + .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(new OperationBuilder( - "post-request-with-parameter", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("POST").param("b&r", "baz") - .param("a", "alpha").build()); + new HttpRequestSnippet() + .document(new OperationBuilder("post-request-with-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("b&r", "baz").param("a", "alpha") + .build()); } @Test public void putRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("put-request-with-content").withContents( - httpRequest(RequestMethod.PUT, "/foo") + this.snippet.expectHttpRequest("put-request-with-content") + .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost").content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(new OperationBuilder( - "put-request-with-content", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").content(content).build()); + new HttpRequestSnippet().document(new OperationBuilder("put-request-with-content", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").content(content).build()); } @Test public void putRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest("put-request-with-parameter").withContents( - httpRequest(RequestMethod.PUT, "/foo") + this.snippet.expectHttpRequest("put-request-with-parameter") + .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(new OperationBuilder( - "put-request-with-parameter", this.snippet.getOutputDirectory()) - .request("http://localhost/foo").method("PUT").param("b&r", "baz") - .param("a", "alpha").build()); + new HttpRequestSnippet() + .document(new OperationBuilder("put-request-with-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("PUT").param("b&r", "baz").param("a", "alpha") + .build()); } @Test public void multipartPost() throws IOException { - String expectedContent = createPart(String.format("Content-Disposition: " - + "form-data; " + "name=image%n%n<< data >>")); - this.snippet.expectHttpRequest("multipart-post").withContents( - httpRequest(RequestMethod.POST, "/upload") + String expectedContent = createPart(String.format( + "Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); + this.snippet + .expectHttpRequest( + "multipart-post") + .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder("multipart-post", - this.snippet.getOutputDirectory()).request("http://localhost/upload") - .method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()).build()); + new HttpRequestSnippet().document( + new OperationBuilder("multipart-post", this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()).build()); } @Test public void multipartPostWithParameters() throws IOException { - String param1Part = createPart(String.format("Content-Disposition: form-data; " - + "name=a%n%napple"), false); - String param2Part = createPart(String.format("Content-Disposition: form-data; " - + "name=a%n%navocado"), false); - String param3Part = createPart(String.format("Content-Disposition: form-data; " - + "name=b%n%nbanana"), false); - String filePart = createPart(String.format("Content-Disposition: form-data; " - + "name=image%n%n<< data >>")); + String param1Part = createPart( + String.format("Content-Disposition: form-data; " + "name=a%n%napple"), + false); + String param2Part = createPart( + String.format("Content-Disposition: form-data; " + "name=a%n%navocado"), + false); + String param3Part = createPart( + String.format("Content-Disposition: form-data; " + "name=b%n%nbanana"), + false); + String filePart = createPart(String + .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = param1Part + param2Part + param3Part + filePart; - this.snippet.expectHttpRequest("multipart-post-with-parameters").withContents( - httpRequest(RequestMethod.POST, "/upload") + this.snippet + .expectHttpRequest( + "multipart-post-with-parameters") + .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a", "apple", "avocado").param("b", "banana") - .part("image", "<< data >>".getBytes()).build()); + new HttpRequestSnippet() + .document(new OperationBuilder("multipart-post-with-parameters", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .param("a", "apple", "avocado").param("b", "banana") + .part("image", "<< data >>".getBytes()).build()); } @Test public void multipartPostWithContentType() throws IOException { - String expectedContent = createPart(String - .format("Content-Disposition: form-data; name=image%nContent-Type: " + String expectedContent = createPart( + String.format("Content-Disposition: form-data; name=image%nContent-Type: " + "image/png%n%n<< data >>")); - this.snippet.expectHttpRequest("multipart-post-with-content-type").withContents( - httpRequest(RequestMethod.POST, "/upload") + this.snippet + .expectHttpRequest( + "multipart-post-with-content-type") + .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document(new OperationBuilder( - "multipart-post-with-content-type", this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE).build()); + new HttpRequestSnippet() + .document(new OperationBuilder("multipart-post-with-content-type", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", "<< data >>".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .build()); } @Test public void getRequestWithCustomHost() throws IOException { - this.snippet.expectHttpRequest("get-request-custom-host").withContents( - httpRequest(RequestMethod.GET, "/foo").header(HttpHeaders.HOST, - "api.example.com")); + this.snippet.expectHttpRequest("get-request-custom-host") + .withContents(httpRequest(RequestMethod.GET, "/foo") + .header(HttpHeaders.HOST, "api.example.com")); new HttpRequestSnippet().document(new OperationBuilder("get-request-custom-host", this.snippet.getOutputDirectory()).request("http://localhost/foo") - .header(HttpHeaders.HOST, "api.example.com").build()); + .header(HttpHeaders.HOST, "api.example.com").build()); } @Test public void requestWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents( - containsString("Title for the request")); + this.snippet.expectHttpRequest("request-with-snippet-attributes") + .withContents(containsString("Title for the request")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-request")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/http-request-with-title.snippet")); new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) .document(new OperationBuilder("request-with-snippet-attributes", this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost/foo").build()); + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost/foo").build()); } private String createPart(String content) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 4959e7774..df19b6bc5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -20,6 +20,7 @@ import org.junit.Rule; import org.junit.Test; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -50,38 +51,41 @@ public class HttpResponseSnippetTests { @Test public void basicResponse() throws IOException { - this.snippet.expectHttpResponse("basic-response").withContents( - httpResponse(HttpStatus.OK)); - new HttpResponseSnippet().document(new OperationBuilder("basic-response", - this.snippet.getOutputDirectory()).build()); + this.snippet.expectHttpResponse("basic-response") + .withContents(httpResponse(HttpStatus.OK)); + new HttpResponseSnippet().document( + new OperationBuilder("basic-response", this.snippet.getOutputDirectory()) + .build()); } @Test public void nonOkResponse() throws IOException { - this.snippet.expectHttpResponse("non-ok-response").withContents( - httpResponse(HttpStatus.BAD_REQUEST)); - new HttpResponseSnippet().document(new OperationBuilder("non-ok-response", - this.snippet.getOutputDirectory()).response() - .status(HttpStatus.BAD_REQUEST.value()).build()); + this.snippet.expectHttpResponse("non-ok-response") + .withContents(httpResponse(HttpStatus.BAD_REQUEST)); + new HttpResponseSnippet().document( + new OperationBuilder("non-ok-response", this.snippet.getOutputDirectory()) + .response().status(HttpStatus.BAD_REQUEST.value()).build()); } @Test public void responseWithHeaders() throws IOException { - this.snippet.expectHttpResponse("response-with-headers").withContents( - httpResponse(HttpStatus.OK).header("Content-Type", "application/json") - .header("a", "alpha")); + this.snippet.expectHttpResponse("response-with-headers") + .withContents(httpResponse(HttpStatus.OK) + .header("Content-Type", "application/json").header("a", "alpha")); new HttpResponseSnippet().document(new OperationBuilder("response-with-headers", - this.snippet.getOutputDirectory()).response() - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) - .header("a", "alpha").build()); + this.snippet.getOutputDirectory()) + .response() + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); } @Test public void responseWithContent() throws IOException { String content = "content"; - this.snippet.expectHttpResponse("response-with-content").withContents( - httpResponse(HttpStatus.OK).content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + this.snippet.expectHttpResponse("response-with-content") + .withContents(httpResponse(HttpStatus.OK).content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpResponseSnippet().document(new OperationBuilder("response-with-content", this.snippet.getOutputDirectory()).response().content(content).build()); } @@ -90,31 +94,31 @@ public void responseWithContent() throws IOException { public void responseWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpResponse("response-with-charset").withContents( - httpResponse(HttpStatus.OK) + this.snippet.expectHttpResponse("response-with-charset") + .withContents(httpResponse(HttpStatus.OK) .header("Content-Type", "text/plain;charset=UTF-8") .content(japaneseContent) .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); new HttpResponseSnippet().document(new OperationBuilder("response-with-charset", this.snippet.getOutputDirectory()).response() - .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) - .build()); + .header("Content-Type", "text/plain;charset=UTF-8") + .content(contentBytes).build()); } @Test public void responseWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents( - containsString("Title for the response")); + this.snippet.expectHttpResponse("response-with-snippet-attributes") + .withContents(containsString("Title for the response")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-response")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/http-response-with-title.snippet")); new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) .document(new OperationBuilder("response-with-snippet-attributes", - this.snippet.getOutputDirectory()).attribute( - TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java index 4ff3dbb4e..9c923e440 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/ContentTypeLinkExtractorTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -47,8 +48,8 @@ public class ContentTypeLinkExtractorTests { @Test public void extractionFailsWithNullContentType() throws IOException { this.thrown.expect(IllegalStateException.class); - new ContentTypeLinkExtractor().extractLinks(this.responseFactory.create( - HttpStatus.OK, new HttpHeaders(), null)); + new ContentTypeLinkExtractor().extractLinks( + this.responseFactory.create(HttpStatus.OK, new HttpHeaders(), null)); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 47a05424a..7353bbeae 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -28,6 +28,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; + import org.springframework.http.HttpStatus; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; @@ -100,7 +101,8 @@ public void linksInTheWrongFormat() throws IOException { assertLinks(Collections.emptyList(), links); } - private void assertLinks(List expectedLinks, Map> actualLinks) { + private void assertLinks(List expectedLinks, + Map> actualLinks) { MultiValueMap expectedLinksByRel = new LinkedMultiValueMap<>(); for (Link expectedLink : expectedLinks) { expectedLinksByRel.add(expectedLink.getRel(), expectedLink); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 07adce26d..5f07e6ddf 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.snippet.SnippetException; @@ -58,22 +59,25 @@ public class LinksSnippetTests { @Test public void undocumentedLink() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Links with the following relations were not" - + " documented: [foo]")); + this.thrown.expectMessage(equalTo( + "Links with the following relations were not" + " documented: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), - Collections.emptyList()).document(new OperationBuilder( - "undocumented-link", this.snippet.getOutputDirectory()).build()); + Collections.emptyList()) + .document(new OperationBuilder("undocumented-link", + this.snippet.getOutputDirectory()).build()); } @Test public void ignoredLink() throws IOException { this.snippet.expectLinks("ignored-link").withContents( tableWithHeader("Relation", "Description").row("b", "Link b")); - new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"), - new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(), - new LinkDescriptor("b").description("Link b"))) - .document(new OperationBuilder("ignored-link", this.snippet - .getOutputDirectory()).build()); + new LinksSnippet( + new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("a").ignored(), + new LinkDescriptor("b").description("Link b"))) + .document(new OperationBuilder("ignored-link", + this.snippet.getOutputDirectory()).build()); } @Test @@ -81,9 +85,10 @@ public void missingLink() throws IOException { this.thrown.expect(SnippetException.class); this.thrown.expectMessage(equalTo("Links with the following relations were not" + " found in the response: [foo]")); - new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") - .description("bar"))).document(new OperationBuilder("missing-link", - this.snippet.getOutputDirectory()).build()); + new LinksSnippet(new StubLinkExtractor(), + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(new OperationBuilder("missing-link", + this.snippet.getOutputDirectory()).build()); } @Test @@ -92,17 +97,18 @@ public void documentedOptionalLink() throws IOException { tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(new OperationBuilder("documented-optional-link", this.snippet - .getOutputDirectory()).build()); + .document(new OperationBuilder("documented-optional-link", + this.snippet.getOutputDirectory()).build()); } @Test public void missingOptionalLink() throws IOException { this.snippet.expectLinks("missing-optional-link").withContents( tableWithHeader("Relation", "Description").row("foo", "bar")); - new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo") - .description("bar").optional())).document(new OperationBuilder( - "missing-optional-link", this.snippet.getOutputDirectory()).build()); + new LinksSnippet(new StubLinkExtractor(), + Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) + .document(new OperationBuilder("missing-optional-link", + this.snippet.getOutputDirectory()).build()); } @Test @@ -112,64 +118,74 @@ public void undocumentedLinkAndMissingLink() throws IOException { + " documented: [a]. Links with the following relations were not" + " found in the response: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), - Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(new OperationBuilder("undocumented-link-and-missing-link", - this.snippet.getOutputDirectory()).build()); + Arrays.asList(new LinkDescriptor("foo").description("bar"))).document( + new OperationBuilder("undocumented-link-and-missing-link", + this.snippet.getOutputDirectory()).build()); } @Test public void documentedLinks() throws IOException { - this.snippet.expectLinks("documented-links").withContents( - tableWithHeader("Relation", "Description").row("a", "one") + this.snippet.expectLinks("documented-links") + .withContents(tableWithHeader("Relation", "Description").row("a", "one") .row("b", "two")); - 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(new OperationBuilder("documented-links", this.snippet - .getOutputDirectory()).build()); + 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(new OperationBuilder("documented-links", + this.snippet.getOutputDirectory()).build()); } @Test public void linksWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); - this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( - tableWithHeader("Relation", "Description", "Foo") + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); + this.snippet.expectLinks("links-with-custom-descriptor-attributes") + .withContents(tableWithHeader("Relation", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "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")))).document(new OperationBuilder( - "links-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()).attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + 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")))) + .document(new OperationBuilder( + "links-with-custom-descriptor-attributes", + this.snippet.getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .build()); } @Test public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) - .willReturn( - new FileSystemResource( - "src/test/resources/custom-snippet-templates/links-with-title.snippet")); - this.snippet.expectLinks("links-with-custom-attributes").withContents( - startsWith(".Title for the links")); - - 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")), attributes(key("title") - .value("Title for the links"))).document(new OperationBuilder( - "links-with-custom-attributes", this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + .willReturn(new FileSystemResource( + "src/test/resources/custom-snippet-templates/links-with-title.snippet")); + this.snippet.expectLinks("links-with-custom-attributes") + .withContents(startsWith(".Title for the links")); + + 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")), + attributes(key("title").value("Title for the links"))) + .document(new OperationBuilder("links-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } private static class StubLinkExtractor implements LinkExtractor { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java index 330381f68..4e45a742f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ContentModifyingOperationPreprocessorTests.java @@ -20,6 +20,7 @@ import java.util.Collections; import org.junit.Test; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java index 622dc5a80..62e69a367 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationRequestPreprocessorTests.java @@ -19,6 +19,7 @@ import java.util.Arrays; import org.junit.Test; + import org.springframework.restdocs.operation.OperationRequest; import static org.hamcrest.CoreMatchers.is; @@ -44,14 +45,14 @@ public void delegationOccurs() { OperationRequest preprocessedRequest3 = mock(OperationRequest.class); given(preprocessor1.preprocess(originalRequest)).willReturn(preprocessedRequest1); - given(preprocessor2.preprocess(preprocessedRequest1)).willReturn( - preprocessedRequest2); - given(preprocessor3.preprocess(preprocessedRequest2)).willReturn( - preprocessedRequest3); + given(preprocessor2.preprocess(preprocessedRequest1)) + .willReturn(preprocessedRequest2); + given(preprocessor3.preprocess(preprocessedRequest2)) + .willReturn(preprocessedRequest3); OperationRequest result = new DelegatingOperationRequestPreprocessor( Arrays.asList(preprocessor1, preprocessor2, preprocessor3)) - .preprocess(originalRequest); + .preprocess(originalRequest); assertThat(result, is(preprocessedRequest3)); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java index 0ddb85a31..422393d62 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/DelegatingOperationResponsePreprocessorTests.java @@ -19,6 +19,7 @@ import java.util.Arrays; import org.junit.Test; + import org.springframework.restdocs.operation.OperationResponse; import static org.hamcrest.CoreMatchers.is; @@ -43,16 +44,16 @@ public void delegationOccurs() { OperationPreprocessor preprocessor3 = mock(OperationPreprocessor.class); OperationResponse preprocessedResponse3 = mock(OperationResponse.class); - given(preprocessor1.preprocess(originalResponse)).willReturn( - preprocessedResponse1); - given(preprocessor2.preprocess(preprocessedResponse1)).willReturn( - preprocessedResponse2); - given(preprocessor3.preprocess(preprocessedResponse2)).willReturn( - preprocessedResponse3); + given(preprocessor1.preprocess(originalResponse)) + .willReturn(preprocessedResponse1); + given(preprocessor2.preprocess(preprocessedResponse1)) + .willReturn(preprocessedResponse2); + given(preprocessor3.preprocess(preprocessedResponse2)) + .willReturn(preprocessedResponse3); OperationResponse result = new DelegatingOperationResponsePreprocessor( Arrays.asList(preprocessor1, preprocessor2, preprocessor3)) - .preprocess(originalResponse); + .preprocess(originalResponse); assertThat(result, is(preprocessedResponse3)); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java index 634ef6162..bd3b07792 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java @@ -21,6 +21,7 @@ import java.util.Collections; import org.junit.Test; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java index 2dfdf64b2..a8d999fc4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/LinkMaskingContentModifierTests.java @@ -22,13 +22,13 @@ import java.util.List; import java.util.Map; -import org.junit.Test; -import org.springframework.restdocs.hypermedia.Link; - import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import org.junit.Test; + +import org.springframework.restdocs.hypermedia.Link; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -59,8 +59,9 @@ public void halLinksAreMasked() throws Exception { @Test public void formattedHalLinksAreMasked() throws Exception { - assertThat(this.contentModifier.modifyContent( - formattedHalPayloadWithLinks(this.links), null), + assertThat( + this.contentModifier + .modifyContent(formattedHalPayloadWithLinks(this.links), null), is(equalTo(formattedHalPayloadWithLinks(this.maskedLinks)))); } @@ -72,16 +73,17 @@ public void atomLinksAreMasked() throws Exception { @Test public void formattedAtomLinksAreMasked() throws Exception { - assertThat(this.contentModifier.modifyContent( - formattedAtomPayloadWithLinks(this.links), null), + assertThat( + this.contentModifier + .modifyContent(formattedAtomPayloadWithLinks(this.links), null), is(equalTo(formattedAtomPayloadWithLinks(this.maskedLinks)))); } @Test public void maskCanBeCustomized() throws Exception { assertThat( - new LinkMaskingContentModifier("custom").modifyContent( - formattedAtomPayloadWithLinks(this.links), null), + new LinkMaskingContentModifier("custom") + .modifyContent(formattedAtomPayloadWithLinks(this.links), null), is(equalTo(formattedAtomPayloadWithLinks(new Link("a", "custom"), new Link("b", "custom"))))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java index 9d37ed78e..f8ea398d3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifierTests.java @@ -20,6 +20,7 @@ import java.util.regex.Pattern; import org.junit.Test; + import org.springframework.http.MediaType; import static org.hamcrest.CoreMatchers.equalTo; @@ -65,8 +66,9 @@ public void encodingIsPreserved() { Pattern pattern = Pattern.compile("[0-9]+"); PatternReplacingContentModifier contentModifier = new PatternReplacingContentModifier( pattern, "<>"); - assertThat(contentModifier.modifyContent((japaneseContent + " 123").getBytes(), - new MediaType("text", "plain", Charset.forName("UTF-8"))), + assertThat( + contentModifier.modifyContent((japaneseContent + " 123").getBytes(), + new MediaType("text", "plain", Charset.forName("UTF-8"))), is(equalTo((japaneseContent + " <>").getBytes()))); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index a06c94006..8e5633a75 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -18,6 +18,7 @@ import org.junit.Rule; import org.junit.Test; + import org.springframework.restdocs.test.OutputCapture; import static org.hamcrest.CoreMatchers.equalTo; @@ -37,25 +38,25 @@ public class PrettyPrintingContentModifierTests { @Test public void prettyPrintJson() throws Exception { - assertThat(new PrettyPrintingContentModifier().modifyContent( - "{\"a\":5}".getBytes(), null), equalTo(String.format("{%n \"a\" : 5%n}") - .getBytes())); + assertThat(new PrettyPrintingContentModifier() + .modifyContent("{\"a\":5}".getBytes(), null), + equalTo(String.format("{%n \"a\" : 5%n}").getBytes())); } @Test public void prettyPrintXml() throws Exception { - assertThat(new PrettyPrintingContentModifier().modifyContent( - "".getBytes(), null), - equalTo(String.format( - "%n" + assertThat( + new PrettyPrintingContentModifier().modifyContent( + "".getBytes(), null), + equalTo(String + .format("%n" + "%n %n%n") - .getBytes())); + .getBytes())); } @Test public void empytContentIsHandledGracefully() throws Exception { - assertThat( - new PrettyPrintingContentModifier().modifyContent("".getBytes(), null), + assertThat(new PrettyPrintingContentModifier().modifyContent("".getBytes(), null), equalTo("".getBytes())); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index 1f9415979..01af20974 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -22,9 +22,8 @@ import java.util.List; import java.util.Map; -import org.junit.Test; - import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; @@ -95,8 +94,8 @@ public void extractNestedArray() { Map entry1 = createEntry("id:1"); Map entry2 = createEntry("id:2"); Map entry3 = createEntry("id:3"); - List>> alpha = Arrays.asList( - Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + List>> alpha = Arrays + .asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][]"), payload), equalTo((Object) Arrays.asList(entry1, entry2, entry3))); @@ -108,8 +107,8 @@ public void extractFromItemsInNestedArray() { Map entry1 = createEntry("id:1"); Map entry2 = createEntry("id:2"); Map entry3 = createEntry("id:3"); - List>> alpha = Arrays.asList( - Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + List>> alpha = Arrays + .asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); assertThat( this.fieldProcessor.extract(JsonFieldPath.compile("a[][].id"), payload), @@ -122,12 +121,13 @@ public void extractArraysFromItemsInNestedArray() { Map entry1 = createEntry("ids", Arrays.asList(1, 2)); Map entry2 = createEntry("ids", Arrays.asList(3)); Map entry3 = createEntry("ids", Arrays.asList(4)); - List>> alpha = Arrays.asList( - Arrays.asList(entry1, entry2), Arrays.asList(entry3)); + List>> alpha = Arrays + .asList(Arrays.asList(entry1, entry2), Arrays.asList(entry3)); payload.put("a", alpha); - assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), - payload), equalTo((Object) Arrays.asList(Arrays.asList(1, 2), - Arrays.asList(3), Arrays.asList(4)))); + assertThat( + this.fieldProcessor.extract(JsonFieldPath.compile("a[][].ids"), payload), + equalTo((Object) Arrays.asList(Arrays.asList(1, 2), Arrays.asList(3), + Arrays.asList(4)))); } @Test(expected = FieldDoesNotExistException.class) @@ -202,8 +202,8 @@ public void removeNestedMapEntry() { @SuppressWarnings("unchecked") @Test public void removeItemsInArray() throws IOException { - Map payload = new ObjectMapper().readValue( - "{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); + Map payload = new ObjectMapper() + .readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); this.fieldProcessor.remove(JsonFieldPath.compile("a[].b"), payload); assertThat(payload.size(), equalTo(0)); } @@ -211,8 +211,8 @@ public void removeItemsInArray() throws IOException { @SuppressWarnings("unchecked") @Test public void removeItemsInNestedArray() throws IOException { - Map payload = new ObjectMapper().readValue( - "{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class); + Map payload = new ObjectMapper() + .readValue("{\"a\": [[{\"id\":1},{\"id\":2}], [{\"id\":3}]]}", Map.class); this.fieldProcessor.remove(JsonFieldPath.compile("a[][].id"), payload); assertThat(payload.size(), equalTo(0)); } @@ -223,8 +223,8 @@ public void extractNestedEntryWithDotInKeys() throws IOException { Map alpha = new HashMap<>(); payload.put("a.key", alpha); alpha.put("b.key", "bravo"); - assertThat(this.fieldProcessor.extract( - JsonFieldPath.compile("['a.key']['b.key']"), payload), + assertThat(this.fieldProcessor + .extract(JsonFieldPath.compile("['a.key']['b.key']"), payload), equalTo((Object) "bravo")); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java index 7ef74aa7f..502bfd29c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java @@ -19,12 +19,11 @@ import java.io.IOException; import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import com.fasterxml.jackson.databind.ObjectMapper; - import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; @@ -73,30 +72,33 @@ public void stringField() throws IOException { @Test public void nestedField() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType("a.b.c", - createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), + assertThat( + this.fieldTypeResolver.resolveFieldType("a.b.c", + createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(JsonFieldType.OBJECT)); } @Test public void multipleFieldsWithSameType() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType("a[].id", - createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")), + assertThat( + this.fieldTypeResolver.resolveFieldType("a[].id", + createPayload("{\"a\":[{\"id\":1},{\"id\":2}]}")), equalTo(JsonFieldType.NUMBER)); } @Test public void multipleFieldsWithDifferentTypes() throws IOException { - assertThat(this.fieldTypeResolver.resolveFieldType("a[].id", - createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")), + assertThat( + this.fieldTypeResolver.resolveFieldType("a[].id", + createPayload("{\"a\":[{\"id\":1},{\"id\":true}]}")), equalTo(JsonFieldType.VARIES)); } @Test public void nonExistentFieldProducesIllegalArgumentException() throws IOException { this.thrownException.expect(FieldDoesNotExistException.class); - this.thrownException - .expectMessage("The payload does not contain a field with the path 'a.b'"); + this.thrownException.expectMessage( + "The payload does not contain a field with the path 'a.b'"); this.fieldTypeResolver.resolveFieldType("a.b", createPayload("{\"a\":{}}")); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 0fedbdee4..40097f3df 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -58,69 +59,74 @@ public class RequestFieldsSnippetTests { @Test public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields("map-request-with-fields").withContents( - tableWithHeader("Path", "Type", "Description") + 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(new OperationBuilder( - "map-request-with-fields", this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + fieldWithPath("a").description("three"))) + .document(new OperationBuilder("map-request-with-fields", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test public void arrayRequestWithFields() throws IOException { - this.snippet.expectRequestFields("array-request-with-fields").withContents( - tableWithHeader("Path", "Type", "Description") + 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(new OperationBuilder( - "array-request-with-fields", this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]").build()); + fieldWithPath("[]a.c").description("two"), + fieldWithPath("[]a").description("three"))) + .document(new OperationBuilder("array-request-with-fields", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content( + "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") + .build()); } @Test public void undocumentedRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-request-field", this.snippet - .getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5}").build()); + .document(new OperationBuilder("undocumented-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("{\"a\": 5}").build()); } @Test public void ignoredRequestField() throws IOException { - this.snippet.expectRequestFields("ignored-request-field").withContents( - tableWithHeader("Path", "Type", "Description").row("b", "Number", - "Field b")); + this.snippet.expectRequestFields("ignored-request-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(new OperationBuilder("ignored-request-field", this.snippet - .getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5, \"b\": 4}").build()); + .document(new OperationBuilder("ignored-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("{\"a\": 5, \"b\": 4}").build()); } @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]")); + 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(new OperationBuilder("missing-request-fields", this.snippet - .getOutputDirectory()).request("http://localhost").content("{}") - .build()); + .document(new OperationBuilder("missing-request-fields", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("{}").build()); } @Test @@ -128,17 +134,16 @@ public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .optional())).document(new OperationBuilder( - "missing-optional-request-field-with-no-type", this.snippet - .getOutputDirectory()).request("http://localhost").content("{ }") - .build()); + "missing-optional-request-field-with-no-type", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("{ }").build()); } @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(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]")); @@ -146,153 +151,163 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException .document(new OperationBuilder( "undocumented-request-field-and-missing-request-field", this.snippet.getOutputDirectory()).request("http://localhost") - .content("{ \"a\": { \"c\": 5 }}").build()); + .content("{ \"a\": { \"c\": 5 }}").build()); } @Test public void requestFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-extra-column")); - this.snippet.expectRequestFields( - "request-fields-with-custom-descriptor-attributes").withContents( - tableWithHeader("Path", "Type", "Description", "Foo") + given(resolver.resolveTemplateResource("request-fields")) + .willReturn(snippetResource("request-fields-with-extra-column")); + 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")); 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(new OperationBuilder( - "request-fields-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).request("http://localhost") - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + 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(new OperationBuilder( + "request-fields-with-custom-descriptor-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-title")); + given(resolver.resolveTemplateResource("request-fields")) + .willReturn(snippetResource("request-fields-with-title")); this.snippet.expectRequestFields("request-fields-with-custom-attributes") .withContents(startsWith(".Custom title")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("request-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}").build()); + attributes(key("title").value("Custom title"))).document( + new OperationBuilder("request-fields-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } @Test public void requestFieldsWithListDescription() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-fields")).willReturn( - snippetResource("request-fields-with-list-description")); + given(resolver.resolveTemplateResource("request-fields")) + .willReturn(snippetResource("request-fields-with-list-description")); this.snippet.expectRequestFields("request-fields-with-list-description") - .withContents( - tableWithHeader("Path", "Type", "Description") - // - .row("a", "String", String.format(" - one%n - two")) - .configuration("[cols=\"1,1,1a\"]")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description( - Arrays.asList("one", "two")))) - .document(new OperationBuilder("request-fields-with-list-description", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}").build()); + .withContents(tableWithHeader("Path", "Type", "Description") + // + .row("a", "String", String.format(" - one%n - two")) + .configuration("[cols=\"1,1,1a\"]")); + + new RequestFieldsSnippet(Arrays.asList( + fieldWithPath("a").description(Arrays.asList("one", "two")))).document( + new OperationBuilder("request-fields-with-list-description", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } @Test public void xmlRequestFields() throws IOException { - this.snippet.expectRequestFields("xml-request").withContents( - tableWithHeader("Path", "Type", "Description").row("a/b", "b", "one") - .row("a/c", "c", "two").row("a", "a", "three")); - - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") - .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-request", this.snippet - .getOutputDirectory()) - .request("http://localhost") - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.expectRequestFields("xml-request") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a/b", "b", "one").row("a/c", "c", "two") + .row("a", "a", "three")); + + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one").type("b"), + fieldWithPath("a/c").description("two").type("c"), + fieldWithPath("a").description("three") + .type("a"))).document(new OperationBuilder("xml-request", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void undocumentedXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-xml-request-field", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.getOutputDirectory()).request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlRequestFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("missing-xml-request", this.snippet - .getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .document(new OperationBuilder("missing-xml-request", + this.snippet.getOutputDirectory()).request("http://localhost") + .content("5").header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void missingXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/b]")); + 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"), - fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-request-fields", this.snippet.getOutputDirectory()) - .request("http://localhost").content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("one"))) + .document( + new OperationBuilder("missing-xml-request-fields", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test - public void undocumentedXmlRequestFieldAndMissingXmlRequestField() throws IOException { + public void undocumentedXmlRequestFieldAndMissingXmlRequestField() + throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); + 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(new OperationBuilder( "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.getOutputDirectory()).request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index ad6653d38..a33a4fec4 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -58,261 +59,282 @@ public class ResponseFieldsSnippetTests { @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") + 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")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), - fieldWithPath("date").description("two"), fieldWithPath("assets") - .description("three"), + fieldWithPath("date").description("two"), + fieldWithPath("assets").description("three"), fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six"))) - .document(new OperationBuilder("map-response-with-fields", this.snippet - .getOutputDirectory()) - .response() - .content( - "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" - + " [{\"id\":356,\"name\": \"sample\"}]}") - .build()); + .document(new OperationBuilder("map-response-with-fields", + this.snippet.getOutputDirectory()) + .response() + .content( + "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}") + .build()); } @Test public void arrayResponseWithFields() throws IOException { - this.snippet.expectResponseFields("array-response-with-fields").withContents( - tableWithHeader("Path", "Type", "Description") + 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")); - new ResponseFieldsSnippet(Arrays.asList( - fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c") - .description("two"), fieldWithPath("[]a").description("three"))) - .document(new OperationBuilder("array-response-with-fields", this.snippet - .getOutputDirectory()).response() - .content("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") - .build()); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), + fieldWithPath("[]a.c").description("two"), + fieldWithPath("[]a").description("three"))) + .document(new OperationBuilder("array-response-with-fields", + this.snippet.getOutputDirectory()) + .response() + .content( + "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") + .build()); } @Test public void arrayResponse() throws IOException { this.snippet.expectResponseFields("array-response") - .withContents( - tableWithHeader("Path", "Type", "Description").row("[]", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("[]", + "String", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document(new OperationBuilder("array-response", this.snippet - .getOutputDirectory()).response() - .content("[\"a\", \"b\", \"c\"]").build()); + .document(new OperationBuilder("array-response", + this.snippet.getOutputDirectory()).response() + .content("[\"a\", \"b\", \"c\"]").build()); } @Test public void ignoredResponseField() throws IOException { - this.snippet.expectResponseFields("ignored-response-field").withContents( - tableWithHeader("Path", "Type", "Description").row("b", "Number", - "Field b")); + this.snippet.expectResponseFields("ignored-response-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(new OperationBuilder("ignored-response-field", this.snippet - .getOutputDirectory()).response().content("{\"a\": 5, \"b\": 4}") - .build()); + .document(new OperationBuilder("ignored-response-field", + this.snippet.getOutputDirectory()).response() + .content("{\"a\": 5, \"b\": 4}").build()); } @Test public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")).willReturn( - snippetResource("response-fields-with-extra-column")); + given(resolver.resolveTemplateResource("response-fields")) + .willReturn(snippetResource("response-fields-with-extra-column")); 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")); + .withContents(tableWithHeader("Path", "Type", "Description", "Foo") + .row("a.b", "Number", "one", "alpha") + .row("a.c", "String", "two", "bravo") + .row("a", "Object", "three", "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(new OperationBuilder( - "response-fields-with-custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + 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(new OperationBuilder( + "response-fields-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response() + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("response-fields")).willReturn( - snippetResource("response-fields-with-title")); + given(resolver.resolveTemplateResource("response-fields")) + .willReturn(snippetResource("response-fields-with-title")); this.snippet.expectResponseFields("response-fields-with-custom-attributes") .withContents(startsWith(".Custom title")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))) - .document(new OperationBuilder("response-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).response() - .content("{\"a\": \"foo\"}").build()); + attributes(key("title").value("Custom title"))).document( + new OperationBuilder("response-fields-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .response().content("{\"a\": \"foo\"}").build()); } @Test public void xmlResponseFields() throws IOException { - this.snippet.expectResponseFields("xml-response").withContents( - tableWithHeader("Path", "Type", "Description").row("a/b", "b", "one") - .row("a/c", "c", "two").row("a", "a", "three")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one") - .type("b"), fieldWithPath("a/c").description("two").type("c"), - fieldWithPath("a").description("three").type("a"))) - .document(new OperationBuilder("xml-response", this.snippet - .getOutputDirectory()) - .response() - .content("5charlie") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.expectResponseFields("xml-response") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a/b", "b", "one").row("a/c", "c", "two") + .row("a", "a", "three")); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one").type("b"), + fieldWithPath("a/c").description("two").type("c"), + fieldWithPath("a").description("three") + .type("a"))).document(new OperationBuilder("xml-response", + this.snippet.getOutputDirectory()).response() + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void undocumentedXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); - new ResponseFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-xml-response-field", - this.snippet.getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); + new ResponseFieldsSnippet( + Collections.emptyList()) + .document( + new OperationBuilder("undocumented-xml-response-field", + this.snippet.getOutputDirectory()).response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlAttribute() throws IOException { - this.snippet.expectResponseFields("xml-attribute").withContents( - tableWithHeader("Path", "Type", "Description").row("a", "b", "one").row( - "a/@id", "c", "two")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("xml-attribute", this.snippet - .getOutputDirectory()) - .response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.expectResponseFields("xml-attribute") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "b", "one").row("a/@id", "c", "two")); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("xml-attribute", + this.snippet.getOutputDirectory()).response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void missingXmlAttribute() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/@id]")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("missing-xml-attribute", this.snippet - .getOutputDirectory()) - .response() - .content("foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/@id]")); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c"))) + .document(new OperationBuilder("missing-xml-attribute", + this.snippet.getOutputDirectory()).response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void missingOptionalXmlAttribute() throws IOException { - this.snippet.expectResponseFields("missing-optional-xml-attribute").withContents( - tableWithHeader("Path", "Type", "Description").row("a", "b", "one").row( - "a/@id", "c", "two")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("b"), fieldWithPath("a/@id").description("two").type("c") - .optional())).document(new OperationBuilder( - "missing-optional-xml-attribute", this.snippet.getOutputDirectory()) - .response().content("foo") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + this.snippet.expectResponseFields("missing-optional-xml-attribute") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "b", "one").row("a/@id", "c", "two")); + new ResponseFieldsSnippet(Arrays.asList( + fieldWithPath("a").description("one").type("b"), + fieldWithPath("a/@id").description("two").type("c").optional())) + .document( + new OperationBuilder("missing-optional-xml-attribute", + this.snippet.getOutputDirectory()).response() + .content("foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void undocumentedAttributeDoesNotCauseFailure() throws IOException { this.snippet.expectResponseFields("undocumented-attribute").withContents( tableWithHeader("Path", "Type", "Description").row("a", "a", "one")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type("a"))).document(new OperationBuilder("undocumented-attribute", - this.snippet.getOutputDirectory()).response() - .content("bar") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a").description("one").type("a"))) + .document( + new OperationBuilder("undocumented-attribute", + this.snippet.getOutputDirectory()).response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void documentedXmlAttributesAreRemoved() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo(String - .format("The following parts of the payload were not documented:" + this.thrown.expectMessage(equalTo( + String.format("The following parts of the payload were not documented:" + "%nbar%n"))); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/@id").description("one") - .type("a"))).document(new OperationBuilder( - "documented-attribute-is-removed", this.snippet.getOutputDirectory()) - .response().content("bar") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet( + Arrays.asList( + fieldWithPath("a/@id").description("one") + .type("a"))).document(new OperationBuilder( + "documented-attribute-is-removed", + this.snippet.getOutputDirectory()).response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlResponseFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("xml-response-no-field-type", this.snippet - .getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .document(new OperationBuilder("xml-response-no-field-type", + this.snippet.getOutputDirectory()).response().content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void missingXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(equalTo("Fields with the following paths were not found" - + " in the payload: [a/b]")); + this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + + " in the payload: [a/b]")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))).document(new OperationBuilder( - "missing-xml-response-field", this.snippet.getOutputDirectory()) - .response().content("") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("one"))) + .document( + new OperationBuilder("missing-xml-response-field", + this.snippet.getOutputDirectory()).response() + .content("") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void undocumentedXmlResponseFieldAndMissingXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown - .expectMessage(startsWith("The following parts of the payload were not" - + " documented:")); + 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 ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) - .document(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()) - .response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("a/b").description("one"))) + .document( + new OperationBuilder( + "undocumented-xml-request-field-and-missing-xml-request-field", + this.snippet.getOutputDirectory()).response() + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 4f2ee0b23..885ba390d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; @@ -62,8 +63,10 @@ public void undocumentedPathParameter() throws IOException { + " not documented: [a]")); new PathParametersSnippet(Collections.emptyList()) .document(new OperationBuilder("undocumented-path-parameter", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/").build()); + this.snippet.getOutputDirectory()) + .attribute("org.springframework.restdocs.urlTemplate", + "/{a}/") + .build()); } @Test @@ -73,9 +76,10 @@ public void missingPathParameter() throws IOException { + " not found in the request: [a]")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("missing-path-parameter", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/").build()); + .document(new OperationBuilder("missing-path-parameter", + this.snippet.getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", "/") + .build()); } @Test @@ -85,23 +89,26 @@ public void undocumentedAndMissingPathParameters() throws IOException { + " 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(new OperationBuilder( - "undocumented-and-missing-path-parameters", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{b}").build()); + Arrays.asList(parameterWithName("a").description("one"))).document( + new OperationBuilder("undocumented-and-missing-path-parameters", + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{b}") + .build()); } @Test public void pathParameters() throws IOException { this.snippet.expectPathParameters("path-parameters").withContents( - tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("a", - "one").row("b", "two")); - new PathParametersSnippet(Arrays.asList( - parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(new OperationBuilder( - "path-parameters", this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") + .row("a", "one").row("b", "two")); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), + parameterWithName("b").description("two"))).document(new OperationBuilder( + "path-parameters", this.snippet.getOutputDirectory()) + .attribute("org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .build()); } @Test @@ -110,10 +117,11 @@ public void ignoredPathParameter() throws IOException { tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))) - .document(new OperationBuilder("ignored-path-parameter", this.snippet - .getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}").build()); + parameterWithName("b").description("two"))).document(new OperationBuilder( + "ignored-path-parameter", this.snippet.getOutputDirectory()) + .attribute("org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .build()); } @Test @@ -122,59 +130,69 @@ public void pathParametersWithQueryString() throws IOException { .withContents( tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") .row("a", "one").row("b", "two")); - new PathParametersSnippet(Arrays.asList( - parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))) - .document(new OperationBuilder("path-parameters-with-query-string", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", "/{a}/{b}?foo=bar") - .build()); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), + parameterWithName("b").description("two"))).document( + new OperationBuilder("path-parameters-with-query-string", + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}?foo=bar") + .build()); } @Test public void pathParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("path-parameters")).willReturn( - snippetResource("path-parameters-with-extra-column")); - this.snippet.expectPathParameters( - "path-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); - - new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") - .attributes(key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo")))) - .document(new OperationBuilder( - "path-parameters-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + given(resolver.resolveTemplateResource("path-parameters")) + .willReturn(snippetResource("path-parameters-with-extra-column")); + this.snippet + .expectPathParameters("path-parameters-with-custom-descriptor-attributes") + .withContents(tableWithHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha").row("b", "two", "bravo")); + + new PathParametersSnippet(Arrays.asList( + parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), + parameterWithName("b").description("two").attributes( + key("foo").value("bravo")))).document(new OperationBuilder( + "path-parameters-with-custom-descriptor-attributes", + this.snippet.getOutputDirectory()).attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } @Test public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("path-parameters")).willReturn( - snippetResource("path-parameters-with-title")); + given(resolver.resolveTemplateResource("path-parameters")) + .willReturn(snippetResource("path-parameters-with-title")); this.snippet.expectPathParameters("path-parameters-with-custom-attributes") .withContents(startsWith(".The title")); - new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one") - .attributes(key("foo").value("alpha")), parameterWithName("b") - .description("two").attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))) - .document(new OperationBuilder("path-parameters-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).build()); + new PathParametersSnippet( + Arrays.asList( + parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))).document( + new OperationBuilder("path-parameters-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index da61deecc..548b287ca 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.FileSystemResource; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateEngine; @@ -60,9 +61,9 @@ public void undocumentedParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [a]")); new RequestParametersSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-parameter", this.snippet - .getOutputDirectory()).request("http://localhost") - .param("a", "alpha").build()); + .document(new OperationBuilder("undocumented-parameter", + this.snippet.getOutputDirectory()).request("http://localhost") + .param("a", "alpha").build()); } @Test @@ -71,9 +72,11 @@ public void missingParameter() throws IOException { this.thrown .expectMessage(equalTo("Request parameters with the following names were" + " not found in the request: [a]")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document(new OperationBuilder("missing-parameter", this.snippet - .getOutputDirectory()).request("http://localhost").build()); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))) + .document(new OperationBuilder("missing-parameter", + this.snippet.getOutputDirectory()) + .request("http://localhost").build()); } @Test @@ -83,23 +86,27 @@ public void undocumentedAndMissingParameters() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [b]. Request parameters with the following" + " names were not found in the request: [a]")); - new RequestParametersSnippet(Arrays.asList(parameterWithName("a").description( - "one"))).document(new OperationBuilder( - "undocumented-and-missing-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost").param("b", "bravo").build()); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))).document( + new OperationBuilder("undocumented-and-missing-parameters", + this.snippet.getOutputDirectory()) + .request("http://localhost").param("b", "bravo") + .build()); } @Test public void requestParameters() throws IOException { - this.snippet.expectRequestParameters("request-parameters").withContents( - tableWithHeader("Parameter", "Description").row("a", "one").row("b", - "two")); - new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one"), parameterWithName("b") - .description("two"))).document(new OperationBuilder( - "request-parameters", this.snippet.getOutputDirectory()) - .request("http://localhost").param("a", "bravo").param("b", "bravo") - .build()); + this.snippet.expectRequestParameters("request-parameters") + .withContents(tableWithHeader("Parameter", "Description").row("a", "one") + .row("b", "two")); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"), + parameterWithName("b").description("two"))) + .document(new OperationBuilder("request-parameters", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .param("a", "bravo").param("b", "bravo") + .build()); } @Test @@ -108,57 +115,65 @@ public void ignoredRequestParameter() throws IOException { tableWithHeader("Parameter", "Description").row("b", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document(new OperationBuilder("ignored-request-parameter", this.snippet - .getOutputDirectory()).request("http://localhost") - .param("a", "bravo").param("b", "bravo").build()); + .document(new OperationBuilder("ignored-request-parameter", + this.snippet.getOutputDirectory()) + .request("http://localhost").param("a", "bravo") + .param("b", "bravo").build()); } @Test public void requestParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")).willReturn( - snippetResource("request-parameters-with-extra-column")); - this.snippet.expectRequestParameters( - "request-parameters-with-custom-descriptor-attributes").withContents( - tableWithHeader("Parameter", "Description", "Foo").row("a", "one", - "alpha").row("b", "two", "bravo")); + given(resolver.resolveTemplateResource("request-parameters")) + .willReturn(snippetResource("request-parameters-with-extra-column")); + this.snippet + .expectRequestParameters( + "request-parameters-with-custom-descriptor-attributes") + .withContents(tableWithHeader("Parameter", "Description", "Foo") + .row("a", "one", "alpha").row("b", "two", "bravo")); new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo")))).document(new OperationBuilder( - "request-parameters-with-custom-descriptor-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).request("http://localhost") - .param("a", "alpha").param("b", "bravo").build()); + parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo")))) + .document(new OperationBuilder( + "request-parameters-with-custom-descriptor-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .param("a", "alpha").param("b", "bravo") + .build()); } @Test public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); - given(resolver.resolveTemplateResource("request-parameters")).willReturn( - snippetResource("request-parameters-with-title")); + given(resolver.resolveTemplateResource("request-parameters")) + .willReturn(snippetResource("request-parameters-with-title")); this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") .withContents(startsWith(".The title")); - new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one").attributes( - key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo"))), attributes(key("title").value( - "The title"))).document(new OperationBuilder( - "request-parameters-with-custom-attributes", this.snippet - .getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)).request("http://localhost") - .param("a", "alpha").param("b", "bravo").build()); + new RequestParametersSnippet( + Arrays.asList( + parameterWithName("a").description("one") + .attributes(key("foo").value("alpha")), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))).document( + new OperationBuilder("request-parameters-with-custom-attributes", + this.snippet.getOutputDirectory()) + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").param("a", "alpha") + .param("b", "bravo").build()); } private FileSystemResource snippetResource(String name) { - return new FileSystemResource("src/test/resources/custom-snippet-templates/" - + name + ".snippet"); + return new FileSystemResource( + "src/test/resources/custom-snippet-templates/" + name + ".snippet"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index b70e936d4..def767471 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.snippet; import org.junit.Test; + import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -33,22 +34,19 @@ public class RestDocumentationContextPlaceholderResolverTests { @Test public void kebabCaseMethodName() throws Exception { - assertThat( - createResolver("dashSeparatedMethodName").resolvePlaceholder( - "method-name"), equalTo("dash-separated-method-name")); + assertThat(createResolver("dashSeparatedMethodName").resolvePlaceholder( + "method-name"), equalTo("dash-separated-method-name")); } @Test public void snakeCaseMethodName() throws Exception { - assertThat( - createResolver("underscoreSeparatedMethodName").resolvePlaceholder( - "method_name"), equalTo("underscore_separated_method_name")); + assertThat(createResolver("underscoreSeparatedMethodName").resolvePlaceholder( + "method_name"), equalTo("underscore_separated_method_name")); } @Test public void camelCaseMethodName() throws Exception { - assertThat( - createResolver("camelCaseMethodName").resolvePlaceholder("methodName"), + assertThat(createResolver("camelCaseMethodName").resolvePlaceholder("methodName"), equalTo("camelCaseMethodName")); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 55ae2218e..e3152d7b0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -19,6 +19,7 @@ import java.io.File; import org.junit.Test; + import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -34,7 +35,8 @@ */ public class StandardWriterResolverTests { - private final PlaceholderResolver placeholderResolver = mock(PlaceholderResolver.class); + private final PlaceholderResolver placeholderResolver = mock( + PlaceholderResolver.class); private final StandardWriterResolver resolver = new StandardWriterResolver( this.placeholderResolver); @@ -48,26 +50,29 @@ public void noConfiguredOutputDirectoryAndRelativeInput() { @Test public void absoluteInput() { String absolutePath = new File("foo").getAbsolutePath(); - assertThat(this.resolver.resolveFile(absolutePath, "bar.txt", - new RestDocumentationContext(null, null, null)), is(new File( - absolutePath, "bar.txt"))); + assertThat( + this.resolver.resolveFile(absolutePath, "bar.txt", + new RestDocumentationContext(null, null, null)), + is(new File(absolutePath, "bar.txt"))); } @Test public void configuredOutputAndRelativeInput() { File outputDir = new File("foo").getAbsoluteFile(); - assertThat(this.resolver.resolveFile("bar", "baz.txt", - new RestDocumentationContext(null, null, outputDir)), is(new File( - outputDir, "bar/baz.txt"))); + assertThat( + this.resolver.resolveFile("bar", "baz.txt", + new RestDocumentationContext(null, null, outputDir)), + is(new File(outputDir, "bar/baz.txt"))); } @Test public void configuredOutputAndAbsoluteInput() { File outputDir = new File("foo").getAbsoluteFile(); String absolutePath = new File("bar").getAbsolutePath(); - assertThat(this.resolver.resolveFile(absolutePath, "baz.txt", - new RestDocumentationContext(null, null, outputDir)), is(new File( - absolutePath, "baz.txt"))); + assertThat( + this.resolver.resolveFile(absolutePath, "baz.txt", + new RestDocumentationContext(null, null, outputDir)), + is(new File(absolutePath, "baz.txt"))); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java index f356d2da3..4213da4e5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -21,6 +21,7 @@ import java.util.Map; import org.junit.Test; + import org.springframework.restdocs.operation.Operation; import static org.hamcrest.CoreMatchers.equalTo; @@ -55,10 +56,8 @@ public void nullAttributesAreTolerated() { @Test public void snippetName() { - assertThat( - new TestTemplatedSnippet(Collections.emptyMap()) - .getSnippetName(), - is(equalTo("test"))); + assertThat(new TestTemplatedSnippet(Collections.emptyMap()) + .getSnippetName(), is(equalTo("test"))); } private static class TestTemplatedSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java index ef807ee20..52e0138b6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/StandardTemplateResourceResolverTests.java @@ -24,6 +24,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; + import org.springframework.core.io.Resource; import static org.hamcrest.CoreMatchers.equalTo; @@ -47,8 +48,8 @@ public class StandardTemplateResourceResolverTests { @Test public void customSnippetResolution() throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/test.snippet", getClass() - .getResource("test.snippet")); + "org/springframework/restdocs/templates/test.snippet", + getClass().getResource("test.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { @@ -66,8 +67,8 @@ public Resource call() { @Test public void fallsBackToDefaultSnippet() throws Exception { this.classLoader.addResource( - "org/springframework/restdocs/templates/default-test.snippet", getClass() - .getResource("test.snippet")); + "org/springframework/restdocs/templates/default-test.snippet", + getClass().getResource("test.snippet")); Resource snippet = doWithThreadContextClassLoader(this.classLoader, new Callable() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 63364f6a6..111096b56 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -23,6 +23,7 @@ import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; + import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; @@ -48,8 +49,8 @@ public class ExpectedSnippet implements TestRule { @Override public Statement apply(final Statement base, Description description) { - this.outputDirectory = new File("build/" - + description.getTestClass().getSimpleName()); + this.outputDirectory = new File( + "build/" + description.getTestClass().getSimpleName()); return new ExpectedSnippetStatement(base); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index e5be81665..5a71d26c0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -90,9 +90,9 @@ public Operation build() { this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( new RestDocumentationContextPlaceholderResolver(context))); return new StandardOperation(this.name, - (this.requestBuilder == null ? new OperationRequestBuilder( - "http://localhost/").buildRequest() : this.requestBuilder - .buildRequest()), + (this.requestBuilder == null + ? new OperationRequestBuilder("http://localhost/").buildRequest() + : this.requestBuilder.buildRequest()), this.responseBuilder.buildResponse(), this.attributes); } @@ -182,7 +182,8 @@ private OperationRequestPartBuilder(String name, byte[] content) { this.content = content; } - public OperationRequestPartBuilder submittedFileName(String submittedFileName) { + public OperationRequestPartBuilder submittedFileName( + String submittedFileName) { this.submittedFileName = submittedFileName; return this; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 97818a52e..98c1ccd10 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -29,6 +29,7 @@ import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; + import org.springframework.http.HttpStatus; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; @@ -71,8 +72,8 @@ public static AsciidoctorCodeBlockMatcher codeBlock(String language) { return new AsciidoctorCodeBlockMatcher(language); } - private static abstract class AbstractSnippetContentMatcher extends - BaseMatcher { + private static abstract class AbstractSnippetContentMatcher + extends BaseMatcher { private List lines = new ArrayList<>(); @@ -149,8 +150,8 @@ public T content(String content) { * * @param The type of the matcher */ - public static abstract class HttpMatcher> extends - AsciidoctorCodeBlockMatcher> { + public static abstract class HttpMatcher> + extends AsciidoctorCodeBlockMatcher> { private int headerOffset = 3; @@ -175,8 +176,8 @@ public T header(String name, long value) { /** * A {@link Matcher} for an HTTP response. */ - public static final class HttpResponseMatcher extends - HttpMatcher { + public static final class HttpResponseMatcher + extends HttpMatcher { private HttpResponseMatcher(HttpStatus status) { this.content("HTTP/1.1 " + status.value() + " " + status.getReasonPhrase()); @@ -200,17 +201,16 @@ private HttpRequestMatcher(RequestMethod requestMethod, String uri) { /** * A {@link Matcher} for an Asciidoctor table. */ - public static final class AsciidoctorTableMatcher extends - AbstractSnippetContentMatcher { + public static final class AsciidoctorTableMatcher + extends AbstractSnippetContentMatcher { private AsciidoctorTableMatcher(String title, String... columns) { if (StringUtils.hasText(title)) { this.addLine("." + title); } this.addLine("|==="); - String header = "|" - + StringUtils - .collectionToDelimitedString(Arrays.asList(columns), "|"); + String header = "|" + StringUtils + .collectionToDelimitedString(Arrays.asList(columns), "|"); this.addLine(header); this.addLine(""); this.addLine("|==="); @@ -258,8 +258,8 @@ private boolean snippetFileExists(Object item) { } private String read(File snippetFile) throws IOException { - return FileCopyUtils.copyToString(new InputStreamReader(new FileInputStream( - snippetFile), "UTF-8")); + return FileCopyUtils.copyToString( + new InputStreamReader(new FileInputStream(snippetFile), "UTF-8")); } @Override @@ -269,12 +269,12 @@ public void describeMismatch(Object item, Description description) { } else if (this.expectedContents != null) { try { - this.expectedContents - .describeMismatch(read((File) item), description); + this.expectedContents.describeMismatch(read((File) item), + description); } catch (IOException e) { - description.appendText("The contents of " + item - + " cound not be read"); + description + .appendText("The contents of " + item + " cound not be read"); } } } diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java index 9449354a8..3045131d0 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactory.java @@ -89,7 +89,8 @@ private List extractParts(MockHttpServletRequest servletRe List parts = new ArrayList<>(); parts.addAll(extractServletRequestParts(servletRequest)); if (servletRequest instanceof MockMultipartHttpServletRequest) { - parts.addAll(extractMultipartRequestParts((MockMultipartHttpServletRequest) servletRequest)); + parts.addAll(extractMultipartRequestParts( + (MockMultipartHttpServletRequest) servletRequest)); } return parts; } @@ -103,24 +104,24 @@ private List extractServletRequestParts( return parts; } - private OperationRequestPart createOperationRequestPart(Part part) throws IOException { + private OperationRequestPart createOperationRequestPart(Part part) + throws IOException { HttpHeaders partHeaders = extractHeaders(part); List contentTypeHeader = partHeaders.get(HttpHeaders.CONTENT_TYPE); if (part.getContentType() != null && contentTypeHeader == null) { partHeaders.setContentType(MediaType.parseMediaType(part.getContentType())); } - return new OperationRequestPartFactory() - .create(part.getName(), - StringUtils.hasText(part.getSubmittedFileName()) ? part - .getSubmittedFileName() : null, FileCopyUtils - .copyToByteArray(part.getInputStream()), partHeaders); + return new OperationRequestPartFactory().create(part.getName(), + StringUtils.hasText(part.getSubmittedFileName()) + ? part.getSubmittedFileName() : null, + FileCopyUtils.copyToByteArray(part.getInputStream()), partHeaders); } private List extractMultipartRequestParts( MockMultipartHttpServletRequest multipartRequest) throws IOException { List parts = new ArrayList<>(); - for (Entry> entry : multipartRequest - .getMultiFileMap().entrySet()) { + for (Entry> entry : multipartRequest.getMultiFileMap() + .entrySet()) { for (MultipartFile file : entry.getValue()) { parts.add(createOperationRequestPart(file)); } @@ -134,8 +135,9 @@ private OperationRequestPart createOperationRequestPart(MultipartFile file) if (StringUtils.hasText(file.getContentType())) { partHeaders.setContentType(MediaType.parseMediaType(file.getContentType())); } - return new OperationRequestPartFactory().create(file.getName(), StringUtils - .hasText(file.getOriginalFilename()) ? file.getOriginalFilename() : null, + return new OperationRequestPartFactory().create(file.getName(), + StringUtils.hasText(file.getOriginalFilename()) + ? file.getOriginalFilename() : null, file.getBytes(), partHeaders); } @@ -170,8 +172,10 @@ private HttpHeaders extractHeaders(MockHttpServletRequest servletRequest) { } private boolean isNonStandardPort(MockHttpServletRequest request) { - return (SCHEME_HTTP.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTP) - || (SCHEME_HTTPS.equals(request.getScheme()) && request.getServerPort() != STANDARD_PORT_HTTPS); + return (SCHEME_HTTP.equals(request.getScheme()) + && request.getServerPort() != STANDARD_PORT_HTTP) + || (SCHEME_HTTPS.equals(request.getScheme()) + && request.getServerPort() != STANDARD_PORT_HTTPS); } private String getRequestUri(MockHttpServletRequest request) { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java index 20f06771c..6bca090a3 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationMockMvcConfigurer.java @@ -108,7 +108,8 @@ public SnippetConfigurer snippets() { * @param templateEngine the template engine to use * @return {@code this} */ - public RestDocumentationMockMvcConfigurer templateEngine(TemplateEngine templateEngine) { + public RestDocumentationMockMvcConfigurer templateEngine( + TemplateEngine templateEngine) { this.templateEngineConfigurer.setTemplateEngine(templateEngine); return this; } @@ -120,7 +121,8 @@ public RestDocumentationMockMvcConfigurer templateEngine(TemplateEngine template * @param writerResolver The writer resolver to use * @return {@code this} */ - public RestDocumentationMockMvcConfigurer writerResolver(WriterResolver writerResolver) { + public RestDocumentationMockMvcConfigurer writerResolver( + WriterResolver writerResolver) { this.writerResolverConfigurer.setWriterResolver(writerResolver); return this; } @@ -157,9 +159,8 @@ void apply(MockHttpServletRequest request) { if (resolverToUse == null) { resolverToUse = new StandardWriterResolver( new RestDocumentationContextPlaceholderResolver( - (RestDocumentationContext) request - .getAttribute(RestDocumentationContext.class - .getName()))); + (RestDocumentationContext) request.getAttribute( + RestDocumentationContext.class.getName()))); } request.setAttribute(WriterResolver.class.getName(), resolverToUse); } @@ -170,8 +171,8 @@ void setWriterResolver(WriterResolver writerResolver) { } - private static final class ConfigurerApplyingRequestPostProcessor implements - RequestPostProcessor { + private static final class ConfigurerApplyingRequestPostProcessor + implements RequestPostProcessor { private final RestDocumentation restDocumentation; diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java index 2c9dc4529..23f8b7936 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuilders.java @@ -54,8 +54,8 @@ private RestDocumentationRequestBuilders() { */ public static MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.get(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.get(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -78,8 +78,8 @@ public static MockHttpServletRequestBuilder get(URI uri) { */ public static MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.post(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.post(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -102,8 +102,8 @@ public static MockHttpServletRequestBuilder post(URI uri) { */ public static MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.put(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.put(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -126,8 +126,8 @@ public static MockHttpServletRequestBuilder put(URI uri) { */ public static MockHttpServletRequestBuilder patch(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.patch(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.patch(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -150,8 +150,8 @@ public static MockHttpServletRequestBuilder patch(URI uri) { */ public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.delete(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.delete(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -174,8 +174,8 @@ public static MockHttpServletRequestBuilder delete(URI uri) { */ public static MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.options(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.options(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -198,8 +198,8 @@ public static MockHttpServletRequestBuilder options(URI uri) { */ public static MockHttpServletRequestBuilder head(String urlTemplate, Object... urlVariables) { - return MockMvcRequestBuilders.head(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + return MockMvcRequestBuilders.head(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** @@ -249,8 +249,8 @@ public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, URI u public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables) { return (MockMultipartHttpServletRequestBuilder) MockMvcRequestBuilders - .fileUpload(urlTemplate, urlVariables).requestAttr( - ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); + .fileUpload(urlTemplate, urlVariables) + .requestAttr(ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate); } /** diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 425cd2fc0..962f12b47 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -59,14 +59,14 @@ public class RestDocumentationResultHandler implements ResultHandler { RestDocumentationResultHandler(String identifier, OperationRequestPreprocessor requestPreprocessor, Snippet... snippets) { - this(identifier, requestPreprocessor, - new IdentityOperationResponsePreprocessor(), snippets); + this(identifier, requestPreprocessor, new IdentityOperationResponsePreprocessor(), + snippets); } RestDocumentationResultHandler(String identifier, OperationResponsePreprocessor responsePreprocessor, Snippet... snippets) { - this(identifier, new IdentityOperationRequestPreprocessor(), - responsePreprocessor, snippets); + this(identifier, new IdentityOperationRequestPreprocessor(), responsePreprocessor, + snippets); } RestDocumentationResultHandler(String identifier, @@ -116,15 +116,15 @@ public RestDocumentationResultHandler snippets(Snippet... snippets) { @SuppressWarnings("unchecked") private List getSnippets(MvcResult result) { - List combinedSnippets = new ArrayList<>((List) result - .getRequest().getAttribute( + List combinedSnippets = new ArrayList<>( + (List) result.getRequest().getAttribute( "org.springframework.restdocs.mockmvc.defaultSnippets")); combinedSnippets.addAll(this.snippets); return combinedSnippets; } - private static final class IdentityOperationRequestPreprocessor implements - OperationRequestPreprocessor { + private static final class IdentityOperationRequestPreprocessor + implements OperationRequestPreprocessor { @Override public OperationRequest preprocess(OperationRequest request) { @@ -133,8 +133,8 @@ public OperationRequest preprocess(OperationRequest request) { } - private static final class IdentityOperationResponsePreprocessor implements - OperationResponsePreprocessor { + private static final class IdentityOperationResponsePreprocessor + implements OperationResponsePreprocessor { @Override public OperationResponse preprocess(OperationResponse response) { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java index 2723d0166..7ac33bd05 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/SnippetConfigurer.java @@ -30,12 +30,11 @@ * * @author Andy Wilkinson */ -public class SnippetConfigurer extends - AbstractNestedConfigurer { +public class SnippetConfigurer + extends AbstractNestedConfigurer { - private List defaultSnippets = Arrays.asList( - CurlDocumentation.curlRequest(), HttpDocumentation.httpRequest(), - HttpDocumentation.httpResponse()); + private List defaultSnippets = Arrays.asList(CurlDocumentation.curlRequest(), + HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse()); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java index b7024fd4c..bbf44345f 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/UriConfigurer.java @@ -23,8 +23,8 @@ * * @author Andy Wilkinson */ -public class UriConfigurer extends - AbstractNestedConfigurer { +public class UriConfigurer + extends AbstractNestedConfigurer { /** * The default scheme for documented URIs. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java index 10c8b9a0c..e587e074f 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/package-info.java @@ -18,4 +18,3 @@ * Core classes for using Spring REST Docs with Spring Test's MockMvc. */ package org.springframework.restdocs.mockmvc; - diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java index 6770d31bb..3ca08471e 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcOperationRequestFactoryTests.java @@ -23,6 +23,7 @@ import javax.servlet.http.Part; import org.junit.Test; + import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; @@ -53,8 +54,8 @@ public class MockMvcOperationRequestFactoryTests { @Test public void httpRequest() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo")); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.get("/foo")); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -71,8 +72,8 @@ public void httpRequestWithCustomPort() throws Exception { @Test public void requestWithContextPath() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders.get( - "/foo/bar").contextPath("/foo")); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.get("/foo/bar").contextPath("/foo")); assertThat(request.getUri(), is(URI.create("http://localhost/foo/bar"))); assertThat(request.getMethod(), is(HttpMethod.GET)); } @@ -124,8 +125,8 @@ public void getRequestWithParametersProducesUriWithQueryString() throws Exceptio @Test public void getRequestWithQueryStringPopulatesParameters() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo?a=alpha&b=bravo")); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.get("/foo?a=alpha&b=bravo")); assertThat(request.getUri(), is(URI.create("http://localhost/foo?a=alpha&b=bravo"))); assertThat(request.getParameters().size(), is(2)); @@ -148,9 +149,9 @@ public void postRequestWithParameters() throws Exception { @Test public void mockMultipartFileUpload() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .fileUpload("/foo").file( - new MockMultipartFile("file", new byte[] { 1, 2, 3, 4 }))); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.fileUpload("/foo") + .file(new MockMultipartFile("file", new byte[] { 1, 2, 3, 4 }))); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.POST)); assertThat(request.getParts().size(), is(1)); @@ -164,10 +165,9 @@ public void mockMultipartFileUpload() throws Exception { @Test public void mockMultipartFileUploadWithContentType() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .fileUpload("/foo").file( - new MockMultipartFile("file", "original", "image/png", - new byte[] { 1, 2, 3, 4 }))); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.fileUpload("/foo").file(new MockMultipartFile( + "file", "original", "image/png", new byte[] { 1, 2, 3, 4 }))); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.POST)); assertThat(request.getParts().size(), is(1)); @@ -186,8 +186,8 @@ public void requestWithPart() throws Exception { given(mockPart.getHeaderNames()).willReturn(Arrays.asList("a", "b")); given(mockPart.getHeaders("a")).willReturn(Arrays.asList("alpha")); given(mockPart.getHeaders("b")).willReturn(Arrays.asList("bravo", "banana")); - given(mockPart.getInputStream()).willReturn( - new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); + given(mockPart.getInputStream()) + .willReturn(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); given(mockPart.getName()).willReturn("part-name"); given(mockPart.getSubmittedFileName()).willReturn("submitted.txt"); mockRequest.addPart(mockPart); @@ -210,8 +210,8 @@ public void requestWithPartWithContentType() throws Exception { given(mockPart.getHeaderNames()).willReturn(Arrays.asList("a", "b")); given(mockPart.getHeaders("a")).willReturn(Arrays.asList("alpha")); given(mockPart.getHeaders("b")).willReturn(Arrays.asList("bravo", "banana")); - given(mockPart.getInputStream()).willReturn( - new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); + given(mockPart.getInputStream()) + .willReturn(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })); given(mockPart.getName()).willReturn("part-name"); given(mockPart.getSubmittedFileName()).willReturn("submitted.png"); given(mockPart.getContentType()).willReturn("image/png"); @@ -229,8 +229,8 @@ public void requestWithPartWithContentType() throws Exception { private OperationRequest createOperationRequest(MockHttpServletRequestBuilder builder) throws Exception { - return this.factory.createOperationRequest(builder - .buildRequest(new MockServletContext())); + return this.factory + .createOperationRequest(builder.buildRequest(new MockServletContext())); } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 76e3dd677..f37ce499a 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -29,6 +29,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -128,12 +129,12 @@ public void curlSnippetWithContent() throws Exception { mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) .andExpect(status().isOk()).andDo(document("curl-snippet-with-content")); - assertThat(new File( - "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( - "$ curl " + "'http://localhost:8080/' -i -X POST " - + "-H 'Accept: application/json' -d 'content'")))); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), + is(snippet().withContents(codeBlock("bash") + .content("$ curl " + "'http://localhost:8080/' -i -X POST " + + "-H 'Accept: application/json' -d 'content'")))); } @Test @@ -141,18 +142,15 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform( - post("/?foo=bar").param("foo", "bar").param("a", "alpha") - .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) .andDo(document("curl-snippet-with-query-string")); assertThat( new File( "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), - is(snippet().withContents( - codeBlock("bash").content( - "$ curl " - + "'http://localhost:8080/?foo=bar' -i -X POST " - + "-H 'Accept: application/json' -d 'a=alpha'")))); + is(snippet().withContents(codeBlock("bash").content( + "$ curl " + "'http://localhost:8080/?foo=bar' -i -X POST " + + "-H 'Accept: application/json' -d 'a=alpha'")))); } @Test @@ -161,8 +159,7 @@ public void linksSnippet() throws Exception { .apply(documentationConfiguration(this.restDocumentation)).build(); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("links", + .andExpect(status().isOk()).andDo(document("links", links(linkWithRel("rel").description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), @@ -176,9 +173,8 @@ public void pathParametersSnippet() throws Exception { .apply(documentationConfiguration(this.restDocumentation)).build(); mockMvc.perform(get("{foo}", "/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("links", pathParameters(parameterWithName("foo") - .description("The description")))); + .andExpect(status().isOk()).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", @@ -191,9 +187,8 @@ public void requestParametersSnippet() throws Exception { .apply(documentationConfiguration(this.restDocumentation)).build(); mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("links", requestParameters(parameterWithName("foo") - .description("The description")))); + .andExpect(status().isOk()).andDo(document("links", requestParameters( + parameterWithName("foo").description("The description")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", @@ -205,12 +200,10 @@ public void requestFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform( - get("/").param("foo", "bar").content("{\"a\":\"alpha\"}") - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("links", - requestFields(fieldWithPath("a").description("The description")))); + mockMvc.perform(get("/").param("foo", "bar").content("{\"a\":\"alpha\"}") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .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", @@ -224,12 +217,10 @@ public void responseFieldsSnippet() throws Exception { mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document( - "links", - responseFields( - 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", @@ -243,8 +234,8 @@ public void parameterizedOutputDirectory() throws Exception { mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("{method-name}")); - assertExpectedSnippetFilesExist(new File( - "build/generated-snippets/parameterized-output-directory"), + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/parameterized-output-directory"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); } @@ -254,20 +245,20 @@ public void multiStep() throws Exception { .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(document("{method-name}-{step}")).build(); - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( - status().isOk()); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); assertExpectedSnippetFilesExist( new File("build/generated-snippets/multi-step-1/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( - status().isOk()); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); assertExpectedSnippetFilesExist( new File("build/generated-snippets/multi-step-2/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); - mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)).andExpect( - status().isOk()); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); assertExpectedSnippetFilesExist( new File("build/generated-snippets/multi-step-3/"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); @@ -280,39 +271,32 @@ public void preprocessedRequest() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); - mockMvc.perform( - get("/").header("a", "alpha").header("b", "bravo") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) - .andExpect(status().isOk()) - .andDo(document("original-request")) - .andDo(document( - "preprocessed-request", - preprocessRequest( - prettyPrint(), + mockMvc.perform(get("/").header("a", "alpha").header("b", "bravo") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) + .andExpect(status().isOk()).andDo(document("original-request")) + .andDo(document("preprocessed-request", + preprocessRequest(prettyPrint(), removeHeaders("a", HttpHeaders.HOST, HttpHeaders.CONTENT_LENGTH), replacePattern(pattern, "\"<>\"")))); assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("a", "alpha") - .header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Host", "localhost") - .header("Content-Length", "13") - .content("{\"a\":\"alpha\"}")))); + is(snippet().withContents(httpRequest(RequestMethod.GET, "/") + .header("a", "alpha").header("b", "bravo") + .header("Content-Type", "application/json") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .header("Host", "localhost").header("Content-Length", "13") + .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet().withContents( - httpRequest(RequestMethod.GET, "/").header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .content(prettyPrinted)))); + is(snippet().withContents(httpRequest(RequestMethod.GET, "/") + .header("b", "bravo").header("Content-Type", "application/json") + .header("Accept", MediaType.APPLICATION_JSON_VALUE) + .content(prettyPrinted)))); } @Test @@ -323,35 +307,29 @@ public void preprocessedResponse() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("original-response")) - .andDo(document( - "preprocessed-response", - preprocessResponse(prettyPrint(), maskLinks(), - removeHeaders("a"), + .andExpect(status().isOk()).andDo(document("original-response")) + .andDo(document("preprocessed-response", + preprocessResponse(prettyPrint(), maskLinks(), removeHeaders("a"), replacePattern(pattern, "\"<>\"")))); String original = "{\"a\":\"alpha\",\"links\":[{\"rel\":\"rel\"," + "\"href\":\"href\"}]}"; assertThat( new File("build/generated-snippets/original-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) - .header("a", "alpha") - .header("Content-Type", "application/json") - .header(HttpHeaders.CONTENT_LENGTH, - original.getBytes().length).content(original)))); + is(snippet().withContents(httpResponse(HttpStatus.OK).header("a", "alpha") + .header("Content-Type", "application/json") + .header(HttpHeaders.CONTENT_LENGTH, original.getBytes().length) + .content(original)))); String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); assertThat( new File( "build/generated-snippets/preprocessed-response/http-response.adoc"), - is(snippet().withContents( - httpResponse(HttpStatus.OK) - .header("Content-Type", "application/json") - .header(HttpHeaders.CONTENT_LENGTH, - prettyPrinted.getBytes().length) - .content(prettyPrinted)))); + is(snippet().withContents(httpResponse(HttpStatus.OK) + .header("Content-Type", "application/json") + .header(HttpHeaders.CONTENT_LENGTH, + prettyPrinted.getBytes().length) + .content(prettyPrinted)))); } @Test @@ -359,8 +337,8 @@ public void customSnippetTemplate() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - ClassLoader classLoader = new URLClassLoader(new URL[] { new File( - "src/test/resources/custom-snippet-templates").toURI().toURL() }, + ClassLoader classLoader = new URLClassLoader(new URL[] { + new File("src/test/resources/custom-snippet-templates").toURI().toURL() }, getClass().getClassLoader()); ClassLoader previous = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); @@ -372,15 +350,13 @@ public void customSnippetTemplate() throws Exception { finally { Thread.currentThread().setContextClassLoader(previous); } - assertThat(new File( - "build/generated-snippets/custom-snippet-template/curl-request.adoc"), + assertThat( + new File( + "build/generated-snippets/custom-snippet-template/curl-request.adoc"), is(snippet().withContents(equalTo("Custom curl request")))); - mockMvc.perform(get("/")).andDo( - document( - "index", - curlRequest(attributes(key("title").value( - "Access the index using curl"))))); + mockMvc.perform(get("/")).andDo(document("index", curlRequest( + attributes(key("title").value("Access the index using curl"))))); } @Test @@ -392,12 +368,10 @@ public void customContextPath() throws Exception { get("/custom/").contextPath("/custom").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andDo(document("custom-context-path")); assertThat( - new File("build/generated-snippets/custom-context-path/curl-request.adoc"), - is(snippet() - .withContents( - codeBlock("bash") - .content( - "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); + new File( + "build/generated-snippets/custom-context-path/curl-request.adoc"), + is(snippet().withContents(codeBlock("bash").content( + "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } private void assertExpectedSnippetFilesExist(File directory, String... snippets) { diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java index 486bfd8e6..eda5107d9 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationConfigurerTests.java @@ -20,6 +20,7 @@ import org.junit.Rule; import org.junit.Test; + import org.springframework.hateoas.mvc.BasicLinkBuilder; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; @@ -58,7 +59,7 @@ public void defaultConfiguration() { public void customScheme() { RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( this.restDocumentation).uris().withScheme("https") - .beforeMockMvcCreated(null, null); + .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); assertUriConfiguration("https", "localhost", 8080); @@ -68,7 +69,7 @@ public void customScheme() { public void customHost() { RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( this.restDocumentation).uris().withHost("api.example.com") - .beforeMockMvcCreated(null, null); + .beforeMockMvcCreated(null, null); postProcessor.postProcessRequest(this.request); assertUriConfiguration("http", "api.example.com", 8080); @@ -77,8 +78,8 @@ public void customHost() { @Test public void customPort() { RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( - this.restDocumentation).uris().withPort(8081) - .beforeMockMvcCreated(null, null); + this.restDocumentation).uris().withPort(8081).beforeMockMvcCreated(null, + null); postProcessor.postProcessRequest(this.request); assertUriConfiguration("http", "localhost", 8081); @@ -87,8 +88,8 @@ public void customPort() { @Test public void noContentLengthHeaderWhenRequestHasNotContent() { RequestPostProcessor postProcessor = new RestDocumentationMockMvcConfigurer( - this.restDocumentation).uris().withPort(8081) - .beforeMockMvcCreated(null, null); + this.restDocumentation).uris().withPort(8081).beforeMockMvcCreated(null, + null); postProcessor.postProcessRequest(this.request); assertThat(this.request.getHeader("Content-Length"), is(nullValue())); } @@ -97,8 +98,8 @@ private void assertUriConfiguration(String scheme, String host, int port) { assertEquals(scheme, this.request.getScheme()); assertEquals(host, this.request.getServerName()); assertEquals(port, this.request.getServerPort()); - RequestContextHolder.setRequestAttributes(new ServletRequestAttributes( - this.request)); + RequestContextHolder + .setRequestAttributes(new ServletRequestAttributes(this.request)); try { URI uri = BasicLinkBuilder.linkToCurrentMapping().toUri(); assertEquals(scheme, uri.getScheme()); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java index a5bd3159f..0ed8fee98 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationRequestBuildersTests.java @@ -21,6 +21,7 @@ import javax.servlet.ServletContext; import org.junit.Test; + import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockServletContext; From cfc413f7ed56613cdd645986c4fd85ff0f8f5a66 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 11:30:28 +0000 Subject: [PATCH 062/898] Avoid byte[] to String to byte[] conversion when pretty printing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, PrettyPrintingContentModifier would convert the byte[] content into a String and then back into a byte[]. It did so without consideration for the content’s character set. As a result, it could fail to preserve the correct character encoding. This commit updates PrettyPrintingContentModifier to avoid converting the content into a String and back into a byte[] and to work entirely with byte arrays instead. Removing the intermediate String from the process removes the possibility of the content becoming corrupted. Closes gh-202 --- .../PrettyPrintingContentModifier.java | 23 +++++++++++-------- .../PrettyPrintingContentModifierTests.java | 19 ++++++++++++++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java index b722038ec..e53ff431e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifier.java @@ -17,8 +17,8 @@ package org.springframework.restdocs.operation.preprocess; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.StringWriter; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -60,7 +60,7 @@ public byte[] modifyContent(byte[] originalContent, MediaType contentType) { if (originalContent.length > 0) { for (PrettyPrinter prettyPrinter : PRETTY_PRINTERS) { try { - return prettyPrinter.prettyPrint(originalContent).getBytes(); + return prettyPrinter.prettyPrint(originalContent); } catch (Exception ex) { // Continue @@ -72,24 +72,25 @@ public byte[] modifyContent(byte[] originalContent, MediaType contentType) { private interface PrettyPrinter { - String prettyPrint(byte[] content) throws Exception; + byte[] prettyPrint(byte[] content) throws Exception; } private static final class XmlPrettyPrinter implements PrettyPrinter { @Override - public String prettyPrint(byte[] original) throws Exception { + public byte[] prettyPrint(byte[] original) throws Exception { Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes"); - StringWriter transformed = new StringWriter(); + ByteArrayOutputStream transformed = new ByteArrayOutputStream(); transformer.setErrorListener(new SilentErrorListener()); transformer.transform(createSaxSource(original), new StreamResult(transformed)); - return transformed.toString(); + + return transformed.toByteArray(); } private SAXSource createSaxSource(byte[] original) @@ -145,11 +146,13 @@ public void fatalError(SAXParseException exception) throws SAXException { private static final class JsonPrettyPrinter implements PrettyPrinter { + private final ObjectMapper objectMapper = new ObjectMapper() + .configure(SerializationFeature.INDENT_OUTPUT, true); + @Override - public String prettyPrint(byte[] original) throws IOException { - ObjectMapper objectMapper = new ObjectMapper() - .configure(SerializationFeature.INDENT_OUTPUT, true); - return objectMapper.writeValueAsString(objectMapper.readTree(original)); + public byte[] prettyPrint(byte[] original) throws IOException { + return this.objectMapper + .writeValueAsBytes(this.objectMapper.readTree(original)); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index 8e5633a75..84accb52a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,12 +16,17 @@ package org.springframework.restdocs.operation.preprocess; +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Rule; import org.junit.Test; import org.springframework.restdocs.test.OutputCapture; import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.isEmptyString; import static org.junit.Assert.assertThat; @@ -68,4 +73,16 @@ public void nonJsonAndNonXmlContentIsHandledGracefully() throws Exception { null), equalTo(content.getBytes())); } + + @Test + public void encodingIsPreserved() throws Exception { + Map input = new HashMap<>(); + input.put("japanese", "\u30b3\u30f3\u30c6\u30f3\u30c4"); + ObjectMapper objectMapper = new ObjectMapper(); + @SuppressWarnings("unchecked") + Map output = objectMapper + .readValue(new PrettyPrintingContentModifier().modifyContent( + objectMapper.writeValueAsBytes(input), null), Map.class); + assertThat(output, is(equalTo(input))); + } } From 4d44401efa0afda9291ddfe536191fd7f0813db7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 12:43:32 +0000 Subject: [PATCH 063/898] Add a note in the documentation about use of the JVM's default charset Closes gh-201 --- docs/src/docs/asciidoc/configuration.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index ae1b601f8..049539969 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -46,6 +46,11 @@ use `RestDocumentationMockMvcConfigurer` to configure it: include::{examples-dir}/com/example/CustomEncoding.java[tags=custom-encoding] ---- +TIP: When Spring REST Docs converts a request or response's content to a String, the +`charset` specified in the `Content-Type` header will be used if it is available. In its +absence, the JVM's default `Charset` will be used. The JVM's default `Charset` can be +configured using the `file.encoding` system property. + [[configuration-default-snippets]] From 40201e2e994d7a3acd2c15f91b96574e6eb6579a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 15:41:09 +0000 Subject: [PATCH 064/898] Improve handling of empty parameters in curl and HTTP request snippets Previously, both the curl and HTTP request snippets would ignore a parameter with no value, for example from the query string of the url http://localhost:8080/foo?bar. This commit updates both snippets so that such parameters are included in the generated snippet, including a multi-part request that is uploading form data and a field in the form has no value. Additions have been made to the tests for both snippets. While the request parameters snippet correctly handled parameters with no value, there was no test verifying that this was the case. One has been added in this commit. Closes gh-200 --- .../restdocs/curl/QueryStringParser.java | 20 +++++-- .../restdocs/http/HttpRequestSnippet.java | 12 +++-- .../restdocs/operation/Parameters.java | 28 +++++++--- .../curl/CurlRequestSnippetTests.java | 36 ++++++++++++- .../http/HttpRequestSnippetTests.java | 52 ++++++++++++++++++- .../RequestParametersSnippetTests.java | 14 ++++- .../restdocs/test/OperationBuilder.java | 12 +++-- 7 files changed, 154 insertions(+), 20 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java index 0982adf95..ea5f31481 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,6 +19,8 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; +import java.util.LinkedList; +import java.util.List; import java.util.Scanner; import org.springframework.restdocs.operation.Parameters; @@ -58,10 +60,18 @@ private Parameters parse(String query) { private void processParameter(String parameter, Parameters parameters) { String[] components = parameter.split("="); - if (components.length == 2) { - String name = components[0]; - String value = components[1]; - parameters.add(decode(name), decode(value)); + if (components.length > 0 && components.length < 3) { + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + List values = parameters.get(components[0]); + if (values == null) { + parameters.put(components[0], new LinkedList()); + } + } } else { throw new IllegalArgumentException( diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 38d1e35f6..cd855f7be 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -127,10 +127,16 @@ private boolean isPutOrPost(OperationRequest request) { private void writeParts(OperationRequest request, PrintWriter writer) { writer.println(); for (Entry> parameter : request.getParameters().entrySet()) { - for (String value : parameter.getValue()) { + if (parameter.getValue().isEmpty()) { writePartBoundary(writer); - writePart(parameter.getKey(), value, null, writer); - writer.println(); + writePart(parameter.getKey(), "", null, writer); + } + else { + for (String value : parameter.getValue()) { + writePartBoundary(writer); + writePart(parameter.getKey(), value, null, writer); + writer.println(); + } } } for (OperationRequestPart part : request.getParts()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java index 9660a9816..0235ec800 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -40,17 +40,33 @@ public class Parameters extends LinkedMultiValueMap { public String toQueryString() { StringBuilder sb = new StringBuilder(); for (Map.Entry> entry : entrySet()) { - for (String value : entry.getValue()) { - if (sb.length() > 0) { - sb.append("&"); + if (entry.getValue().isEmpty()) { + append(sb, entry.getKey()); + } + else { + for (String value : entry.getValue()) { + append(sb, entry.getKey(), value); } - sb.append(urlEncodeUTF8(entry.getKey())).append('=') - .append(urlEncodeUTF8(value)); } } return sb.toString(); } + private static void append(StringBuilder sb, String key) { + append(sb, key, ""); + } + + private static void append(StringBuilder sb, String key, String value) { + doAppend(sb, urlEncodeUTF8(key) + "=" + urlEncodeUTF8(value)); + } + + private static void doAppend(StringBuilder sb, String toAppend) { + if (sb.length() > 0) { + sb.append("&"); + } + sb.append(toAppend); + } + private static String urlEncodeUTF8(String s) { try { return URLEncoder.encode(s, "UTF-8"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index c614cb838..519644337 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -95,6 +95,17 @@ public void getRequestWithQueryString() throws IOException { .request("http://localhost/foo?param=value").build()); } + @Test + public void getRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectCurlRequest("request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param' -i")); + new CurlRequestSnippet() + .document(new OperationBuilder("request-with-query-string-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param").build()); + } + @Test public void postRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("post-request-with-query-string") @@ -107,6 +118,18 @@ public void postRequestWithQueryString() throws IOException { .method("POST").build()); } + @Test + public void postRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectCurlRequest("post-request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param' -i -X POST")); + new CurlRequestSnippet().document( + new OperationBuilder("post-request-with-query-string-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param").method("POST") + .build()); + } + @Test public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter") @@ -118,6 +141,17 @@ public void postRequestWithOneParameter() throws IOException { .method("POST").param("k1", "v1").build()); } + @Test + public void postRequestWithOneParameterWithNoValue() throws IOException { + this.snippet.expectCurlRequest("post-request-with-one-parameter-with-no-value") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); + new CurlRequestSnippet().document( + new OperationBuilder("post-request-with-one-parameter-with-no-value", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("k1").build()); + } + @Test public void postRequestWithMultipleParameters() throws IOException { this.snippet.expectCurlRequest("post-request-with-multiple-parameters") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index a2975674a..5175edd8c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -75,6 +75,18 @@ public void getRequestWithQueryString() throws IOException { .request("http://localhost/foo?bar=baz").build()); } + @Test + public void getRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectHttpRequest("get-request-with-query-string-with-no-value") + .withContents(httpRequest(RequestMethod.GET, "/foo?bar") + .header(HttpHeaders.HOST, "localhost")); + + new HttpRequestSnippet().document( + new OperationBuilder("get-request-with-query-string-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost/foo?bar").build()); + } + @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; @@ -123,6 +135,20 @@ public void postRequestWithParameter() throws IOException { .build()); } + @Test + public void postRequestWithParameterWithNoValue() throws IOException { + this.snippet.expectHttpRequest("post-request-with-parameter") + .withContents(httpRequest(RequestMethod.POST, "/foo") + .header(HttpHeaders.HOST, "localhost") + .header("Content-Type", "application/x-www-form-urlencoded") + .content("bar=")); + + new HttpRequestSnippet() + .document(new OperationBuilder("post-request-with-parameter", + this.snippet.getOutputDirectory()).request("http://localhost/foo") + .method("POST").param("bar").build()); + } + @Test public void putRequestWithContent() throws IOException { String content = "Hello, world"; @@ -201,6 +227,30 @@ public void multipartPostWithParameters() throws IOException { .part("image", "<< data >>".getBytes()).build()); } + @Test + public void multipartPostWithParameterWithNoValue() throws IOException { + String paramPart = createPart( + String.format("Content-Disposition: form-data; " + "name=a%n"), false); + String filePart = createPart(String + .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); + String expectedContent = paramPart + filePart; + this.snippet + .expectHttpRequest( + "multipart-post-with-parameter-with-no-value") + .withContents(httpRequest(RequestMethod.POST, "/upload") + .header("Content-Type", + "multipart/form-data; boundary=" + BOUNDARY) + .header(HttpHeaders.HOST, "localhost").content(expectedContent)); + new HttpRequestSnippet().document( + new OperationBuilder("multipart-post-with-parameter-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .param("a").part("image", "<< data >>".getBytes()) + .build()); + } + @Test public void multipartPostWithContentType() throws IOException { String expectedContent = createPart( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 548b287ca..c4b05f355 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -109,6 +109,18 @@ public void requestParameters() throws IOException { .build()); } + @Test + public void requestParameterWithNoValue() throws IOException { + this.snippet.expectRequestParameters("request-parameter-with-no-value") + .withContents( + tableWithHeader("Parameter", "Description").row("a", "one")); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one"))) + .document(new OperationBuilder("request-parameter-with-no-value", + this.snippet.getOutputDirectory()) + .request("http://localhost").param("a").build()); + } + @Test public void ignoredRequestParameter() throws IOException { this.snippet.expectRequestParameters("ignored-request-parameter").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 5a71d26c0..e483d6c51 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,6 +19,7 @@ import java.io.File; import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -146,8 +147,13 @@ public OperationRequestBuilder content(byte[] content) { } public OperationRequestBuilder param(String name, String... values) { - for (String value : values) { - this.parameters.add(name, value); + if (values.length > 0) { + for (String value : values) { + this.parameters.add(name, value); + } + } + else { + this.parameters.put(name, Collections.emptyList()); } return this; } From 7e2a3dcd18f974bffa8de340a5fa9916270c0566 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 16:01:59 +0000 Subject: [PATCH 065/898] Fix faulty forward merge --- .../curl/CurlRequestSnippetTests.java | 19 ++++++------- .../http/HttpRequestSnippetTests.java | 27 ++++++++----------- .../RequestParametersSnippetTests.java | 5 ++-- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 9447cd3d6..2b1371924 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -93,9 +93,8 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param' -i")); new CurlRequestSnippet() - .document(new OperationBuilder("request-with-query-string-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo?param").build()); + .document(operationBuilder("request-with-query-string-with-no-value") + .request("http://localhost/foo?param").build()); } @Test @@ -114,11 +113,9 @@ public void postRequestWithQueryStringWithNoValue() throws IOException { this.snippet.expectCurlRequest("post-request-with-query-string-with-no-value") .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param' -i -X POST")); - new CurlRequestSnippet().document( - new OperationBuilder("post-request-with-query-string-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo?param").method("POST") - .build()); + new CurlRequestSnippet() + .document(operationBuilder("post-request-with-query-string-with-no-value") + .request("http://localhost/foo?param").method("POST").build()); } @Test @@ -138,9 +135,9 @@ public void postRequestWithOneParameterWithNoValue() throws IOException { .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); new CurlRequestSnippet().document( - new OperationBuilder("post-request-with-one-parameter-with-no-value", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("k1").build()); + operationBuilder("post-request-with-one-parameter-with-no-value") + .request("http://localhost/foo").method("POST").param("k1") + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index c2436ec98..0660eac8b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -76,10 +76,9 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { .withContents(httpRequest(RequestMethod.GET, "/foo?bar") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document( - new OperationBuilder("get-request-with-query-string-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost/foo?bar").build()); + new HttpRequestSnippet() + .document(operationBuilder("get-request-with-query-string-with-no-value") + .request("http://localhost/foo?bar").build()); } @Test @@ -132,10 +131,8 @@ public void postRequestWithParameterWithNoValue() throws IOException { .header("Content-Type", "application/x-www-form-urlencoded") .content("bar=")); - new HttpRequestSnippet() - .document(new OperationBuilder("post-request-with-parameter", - this.snippet.getOutputDirectory()).request("http://localhost/foo") - .method("POST").param("bar").build()); + new HttpRequestSnippet().document(operationBuilder("post-request-with-parameter") + .request("http://localhost/foo").method("POST").param("bar").build()); } @Test @@ -226,14 +223,12 @@ public void multipartPostWithParameterWithNoValue() throws IOException { .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet().document( - new OperationBuilder("multipart-post-with-parameter-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a").part("image", "<< data >>".getBytes()) - .build()); + new HttpRequestSnippet() + .document(operationBuilder("multipart-post-with-parameter-with-no-value") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .param("a").part("image", "<< data >>".getBytes()).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 95a38a2f3..204806522 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -65,9 +65,8 @@ public void requestParameterWithNoValue() throws IOException { tableWithHeader("Parameter", "Description").row("a", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("request-parameter-with-no-value", - this.snippet.getOutputDirectory()) - .request("http://localhost").param("a").build()); + .document(operationBuilder("request-parameter-with-no-value") + .request("http://localhost").param("a").build()); } @Test From c8ecd255913cfeeba251b0de66afcae46b6ab43b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 17:12:16 +0000 Subject: [PATCH 066/898] Add a sample demonstrating how to use REST Assured Closes gh-198 --- build.gradle | 4 + docs/src/docs/asciidoc/getting-started.adoc | 39 +++-- samples/rest-assured/build.gradle | 58 +++++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51017 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + samples/rest-assured/gradlew | 164 ++++++++++++++++++ samples/rest-assured/gradlew.bat | 90 ++++++++++ .../rest-assured/src/docs/asciidoc/index.adoc | 26 +++ .../SampleRestAssuredApplication.java | 41 +++++ .../SampleRestAssuredApplicationTests.java | 74 ++++++++ samples/testng/src/docs/asciidoc/index.adoc | 5 +- 11 files changed, 491 insertions(+), 16 deletions(-) create mode 100644 samples/rest-assured/build.gradle create mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/rest-assured/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/rest-assured/gradlew create mode 100644 samples/rest-assured/gradlew.bat create mode 100644 samples/rest-assured/src/docs/asciidoc/index.adoc create mode 100644 samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java create mode 100644 samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java diff --git a/build.gradle b/build.gradle index 0e8acc927..2439c1ccd 100644 --- a/build.gradle +++ b/build.gradle @@ -166,6 +166,10 @@ samples { testNg { workingDir "$projectDir/samples/testng" } + + restAssured { + workingDir "$projectDir/samples/rest-assured" + } } task api (type: Javadoc) { diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 415836231..485dec89a 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -8,20 +8,31 @@ This section describes how to get started with Spring REST Docs. [[getting-started-sample-applications]] === Sample applications -If you want to jump straight in, there are two sample applications available. -{samples}/rest-notes-spring-hateoas[One sample] uses -http://projects.spring.io/spring-hateoas/[Spring HATEOAS] and -{samples}/rest-notes-spring-data-rest[the other] uses -http://projects.spring.io/spring-data-rest/[Spring Data REST]. Both samples use -Spring REST Docs to produce a detailed API guide and a getting started walkthrough. - -Each sample contains a file named `api-guide.adoc` that produces an API guide for the -service, and a file named `getting-started-guide.adoc` that produces a getting started -guide that provides an introductory walkthrough. - -The code that produces the generated snippets can be found in `src/test/java`. -`ApiDocumentation.java` produces the snippets for the API guide. -`GettingStartedDocumentation.java` produces the snippets for the getting started guide. +If you want to jump straight in, a number of sample applications are available: + +[cols="3,2,10"] +|=== +| Sample | Build system | Description + +| {samples}/rest-assured[REST Assured] +| Gradle +| Demonstrates the use of Spring REST Docs with REST Assured. + +| {samples}/rest-notes-spring-data-rest[Spring Data REST] +| Maven +| Demonstrates the creation of a getting started guide and an API guide for a service + implemented using Spring Data REST. + +| {samples}/rest-notes-spring-hateoas[Spring HATEOAS] +| Gradle +| Demonstrates the creation of a getting started guide and an API guide for a service + implemented using Spring HATEOAS. + +| {samples}/testng[TestNG] +| Gradle +| Demonstrates the use of Spring REST Docs with TestNG. + +|=== diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle new file mode 100644 index 000000000..9ad942399 --- /dev/null +++ b/samples/rest-assured/build.gradle @@ -0,0 +1,58 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + } +} + +plugins { + id "org.asciidoctor.convert" version "1.5.3" +} + +apply plugin: 'java' +apply plugin: 'spring-boot' +apply plugin: 'eclipse' + +repositories { + mavenLocal() + maven { url 'https://repo.spring.io/libs-snapshot' } + mavenCentral() +} + +group = 'com.example' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +ext { + snippetsDir = file('build/generated-snippets') + springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' +} + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-web' + testCompile 'org.springframework.boot:spring-boot-starter-test' + testCompile "org.springframework.restdocs:spring-restdocs-restassured:$springRestdocsVersion" +} + +test { + outputs.dir snippetsDir +} + +asciidoctor { + attributes 'snippets': snippetsDir + inputs.dir snippetsDir + dependsOn test +} + +jar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } +} + +eclipseJdt.onlyIf { false } +cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..3d0dee6e8edfecc92e04653ec780de06f7b34f8b GIT binary patch literal 51017 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 H-~Rf4?cF@p literal 0 HcmV?d00001 diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..2b8cbfe8d --- /dev/null +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Apr 16 12:33:24 BST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew new file mode 100755 index 000000000..91a7e269e --- /dev/null +++ b/samples/rest-assured/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat new file mode 100644 index 000000000..8a0b282aa --- /dev/null +++ b/samples/rest-assured/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/rest-assured/src/docs/asciidoc/index.adoc b/samples/rest-assured/src/docs/asciidoc/index.adoc new file mode 100644 index 000000000..f89aeb6e1 --- /dev/null +++ b/samples/rest-assured/src/docs/asciidoc/index.adoc @@ -0,0 +1,26 @@ += Spring REST Docs REST Assured Sample +Andy Wilkinson; +:doctype: book +:icons: font +:source-highlighter: highlightjs + +Sample application demonstrating how to use Spring REST Docs with REST Assured. + +`SampleRestAssuredApplicationTests` makes a call to a very simple service. The service +that is being tested is running on a random port on `localhost`. The tests make use of a +preprocessor to modify the request so that it appears to have been sent to +`https://api.example.com`. If your service includes URIs in its responses, for example +because it uses hypermedia, similar preprocessing can be applied to the response before +it is documented. + +Three snippets are produced. One showing how to make a request using cURL: + +include::{snippets}/sample/curl-request.adoc[] + +One showing the HTTP request: + +include::{snippets}/sample/http-request.adoc[] + +And one showing the HTTP response: + +include::{snippets}/sample/http-response.adoc[] \ No newline at end of file diff --git a/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java b/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java new file mode 100644 index 000000000..7df98b5f2 --- /dev/null +++ b/samples/rest-assured/src/main/java/com/example/restassured/SampleRestAssuredApplication.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication +public class SampleRestAssuredApplication { + + public static void main(String[] args) { + new SpringApplication(SampleRestAssuredApplication.class).run(args); + } + + @RestController + private static class SampleController { + + @RequestMapping("/") + public String index() { + return "Hello, World"; + } + + } + +} diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java new file mode 100644 index 000000000..92ef63746 --- /dev/null +++ b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import static com.jayway.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; + +@SpringApplicationConfiguration(classes=SampleRestAssuredApplication.class) +@WebIntegrationTest("server.port=0") +@RunWith(SpringJUnit4ClassRunner.class) +public class SampleRestAssuredApplicationTests { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + + private RequestSpecification documentationSpec; + + @Value("${local.server.port}") + private int port; + + @Before + public void setUp() { + this.documentationSpec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation)).build(); + } + + @Test + public void sample() throws Exception { + given(this.documentationSpec) + .accept("text/plain") + .filter(document("sample", + preprocessRequest(modifyUris() + .scheme("https") + .host("api.example.com") + .removePort()))) + .when() + .port(this.port) + .get("/") + .then() + .assertThat().statusCode(is(200)); + } + +} diff --git a/samples/testng/src/docs/asciidoc/index.adoc b/samples/testng/src/docs/asciidoc/index.adoc index ff513a31a..ea8b47e49 100644 --- a/samples/testng/src/docs/asciidoc/index.adoc +++ b/samples/testng/src/docs/asciidoc/index.adoc @@ -4,8 +4,9 @@ Andy Wilkinson; :icons: font :source-highlighter: highlightjs -This sample application demonstrates how to use Spring REST Docs with TestNG. -`SampleTestNgApplication` makes a call to a very simple service and produces three +Sample application demonstrating how to use Spring REST Docs with TestNG. + +`SampleTestNgApplicationTests` makes a call to a very simple service and produces three documentation snippets. One showing how to make a request using cURL: From b224333c977b66c6a22895f47d777bebe69a0f6c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 17:17:36 +0000 Subject: [PATCH 067/898] Upgrade to Gradle 2.11 --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- .../gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- samples/rest-assured/gradlew | 10 +++------- .../gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- samples/rest-notes-spring-hateoas/gradlew | 10 +++------- .../testng/gradle/wrapper/gradle-wrapper.jar | Bin 51017 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 ++-- samples/testng/gradlew | 10 +++------- 10 files changed, 17 insertions(+), 29 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ed0a26aa4..0c9568aee 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Sep 23 15:50:30 BST 2015 +#Mon Feb 15 17:15:12 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar index 3d0dee6e8edfecc92e04653ec780de06f7b34f8b..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 27682 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 diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties index 2b8cbfe8d..72a999e4b 100644 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 16 12:33:24 BST 2015 +#Mon Feb 15 17:16:16 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew index 91a7e269e..9d82f7891 100755 --- a/samples/rest-assured/gradlew +++ b/samples/rest-assured/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar index 3d0dee6e8edfecc92e04653ec780de06f7b34f8b..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 27682 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 diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties index 2b8cbfe8d..347aa08f3 100644 --- a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 16 12:33:24 BST 2015 +#Mon Feb 15 17:16:28 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-notes-spring-hateoas/gradlew b/samples/rest-notes-spring-hateoas/gradlew index 91a7e269e..9d82f7891 100755 --- a/samples/rest-notes-spring-hateoas/gradlew +++ b/samples/rest-notes-spring-hateoas/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.jar b/samples/testng/gradle/wrapper/gradle-wrapper.jar index 3d0dee6e8edfecc92e04653ec780de06f7b34f8b..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 27682 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 diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties index 2b8cbfe8d..9332fb8f6 100644 --- a/samples/testng/gradle/wrapper/gradle-wrapper.properties +++ b/samples/testng/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 16 12:33:24 BST 2015 +#Mon Feb 15 17:16:46 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/testng/gradlew b/samples/testng/gradlew index 91a7e269e..9d82f7891 100755 --- a/samples/testng/gradlew +++ b/samples/testng/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` From ba76c2f843490e69bdca80bdae3b5fe743c2c3ed Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 15 Oct 2015 17:41:18 +0100 Subject: [PATCH 068/898] Add a sample showing how to use Spring REST Docs with Slate Closes gh-124 --- build.gradle | 4 + .../build/SampleBuildConfigurer.groovy | 38 +- docs/src/docs/asciidoc/getting-started.adoc | 13 +- samples/rest-notes-slate/README.md | 24 + samples/rest-notes-slate/build.gradle | 64 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53638 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + samples/rest-notes-slate/gradlew | 160 ++ samples/rest-notes-slate/gradlew.bat | 90 + samples/rest-notes-slate/slate/.gitignore | 22 + samples/rest-notes-slate/slate/CHANGELOG.md | 49 + samples/rest-notes-slate/slate/Gemfile | 12 + samples/rest-notes-slate/slate/Gemfile.lock | 140 ++ samples/rest-notes-slate/slate/LICENSE | 13 + samples/rest-notes-slate/slate/README.md | 128 ++ samples/rest-notes-slate/slate/Rakefile | 6 + samples/rest-notes-slate/slate/config.rb | 41 + .../slate/font-selection.json | 148 ++ .../slate/source/api-guide.md.erb | 206 ++ .../slate/source/fonts/slate.eot | Bin 0 -> 1876 bytes .../slate/source/fonts/slate.svg | 14 + .../slate/source/fonts/slate.ttf | Bin 0 -> 1720 bytes .../slate/source/fonts/slate.woff | Bin 0 -> 1796 bytes .../slate/source/fonts/slate.woff2 | Bin 0 -> 796 bytes .../slate/source/images/logo.png | Bin 0 -> 3507 bytes .../slate/source/images/navbar.png | Bin 0 -> 96 bytes .../slate/source/javascripts/all.js | 4 + .../slate/source/javascripts/all_nosearch.js | 3 + .../slate/source/javascripts/app/_lang.js | 162 ++ .../slate/source/javascripts/app/_search.js | 74 + .../slate/source/javascripts/app/_toc.js | 55 + .../slate/source/javascripts/lib/_energize.js | 169 ++ .../javascripts/lib/_imagesloaded.min.js | 7 + .../javascripts/lib/_jquery.highlight.js | 108 + .../source/javascripts/lib/_jquery.tocify.js | 1042 +++++++++ .../source/javascripts/lib/_jquery_ui.js | 566 +++++ .../slate/source/javascripts/lib/_lunr.js | 1910 +++++++++++++++++ .../slate/source/layouts/layout.erb | 102 + .../slate/source/stylesheets/_icon-font.scss | 38 + .../slate/source/stylesheets/_normalize.css | 427 ++++ .../slate/source/stylesheets/_syntax.scss.erb | 27 + .../slate/source/stylesheets/_variables.scss | 109 + .../slate/source/stylesheets/print.css.scss | 142 ++ .../slate/source/stylesheets/screen.css.scss | 620 ++++++ .../src/main/java/com/example/notes/Note.java | 75 + .../com/example/notes/NoteRepository.java | 23 + .../com/example/notes/RestNotesSlate.java | 33 + .../src/main/java/com/example/notes/Tag.java | 65 + .../java/com/example/notes/TagRepository.java | 23 + .../src/main/resources/application.properties | 1 + .../com/example/notes/ApiDocumentation.java | 343 +++ 51 files changed, 7289 insertions(+), 17 deletions(-) create mode 100644 samples/rest-notes-slate/README.md create mode 100644 samples/rest-notes-slate/build.gradle create mode 100644 samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/rest-notes-slate/gradlew create mode 100644 samples/rest-notes-slate/gradlew.bat create mode 100644 samples/rest-notes-slate/slate/.gitignore create mode 100644 samples/rest-notes-slate/slate/CHANGELOG.md create mode 100644 samples/rest-notes-slate/slate/Gemfile create mode 100644 samples/rest-notes-slate/slate/Gemfile.lock create mode 100644 samples/rest-notes-slate/slate/LICENSE create mode 100644 samples/rest-notes-slate/slate/README.md create mode 100644 samples/rest-notes-slate/slate/Rakefile create mode 100644 samples/rest-notes-slate/slate/config.rb create mode 100755 samples/rest-notes-slate/slate/font-selection.json create mode 100644 samples/rest-notes-slate/slate/source/api-guide.md.erb create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.eot create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.svg create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.ttf create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.woff create mode 100755 samples/rest-notes-slate/slate/source/fonts/slate.woff2 create mode 100644 samples/rest-notes-slate/slate/source/images/logo.png create mode 100644 samples/rest-notes-slate/slate/source/images/navbar.png create mode 100644 samples/rest-notes-slate/slate/source/javascripts/all.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/all_nosearch.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_lang.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_search.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/app/_toc.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js create mode 100644 samples/rest-notes-slate/slate/source/layouts/layout.erb create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_normalize.css create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_variables.scss create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/print.css.scss create mode 100644 samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/Note.java create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java create mode 100644 samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java create mode 100644 samples/rest-notes-slate/src/main/resources/application.properties create mode 100644 samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java diff --git a/build.gradle b/build.gradle index 2439c1ccd..67a98120c 100644 --- a/build.gradle +++ b/build.gradle @@ -216,6 +216,10 @@ task docsZip(type: Zip, dependsOn: [':docs:asciidoctor', ':api', ':buildSamples' from(file('samples/rest-notes-spring-hateoas/build/asciidoc/html5')) { into 'samples/restful-notes' } + + from(file('samples/rest-notes-slate/build/docs')) { + into 'samples/slate' + } } configurations { diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 5ddd17ab1..c960ee1ea 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -29,6 +29,8 @@ public class SampleBuildConfigurer { private String workingDir + private boolean verifyIncludes = true + SampleBuildConfigurer(String name) { this.name = name } @@ -39,24 +41,34 @@ public class SampleBuildConfigurer { Task createTask(Project project, Object... dependencies) { File sampleDir = new File(this.workingDir).absoluteFile - Task verifyIncludes + Task sampleBuild = project.tasks.create name + sampleBuild.description = "Builds the ${name} sample" + sampleBuild.group = "Build" if (new File(sampleDir, 'build.gradle').isFile()) { Task gradleBuild = createGradleBuild(project, dependencies) - verifyIncludes = createVerifyIncludes(project, new File(sampleDir, 'build/asciidoc')) - verifyIncludes.dependsOn gradleBuild + if (verifyIncludes) { + Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'build/asciidoc')) + verifyIncludesTask.dependsOn gradleBuild + sampleBuild.dependsOn verifyIncludesTask + } + else { + sampleBuild.dependsOn gradleBuild + } } else if (new File(sampleDir, 'pom.xml').isFile()) { Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) - verifyIncludes = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) - verifyIncludes.dependsOn(mavenBuild) + if (verifyIncludes) { + Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) + verifyIncludesTask.dependsOn(mavenBuild) + sampleBuild.dependsOn verifyIncludesTask + } + else { + sampleBuild.dependsOn mavenBuild + } } else { throw new IllegalStateException("No pom.xml or build.gradle was found in $sampleDir") } - Task sampleBuild = project.tasks.create name - sampleBuild.description = "Builds the ${name} sample" - sampleBuild.group = "Build" - sampleBuild.dependsOn verifyIncludes return sampleBuild } @@ -108,9 +120,9 @@ public class SampleBuildConfigurer { } private Task createVerifyIncludes(Project project, File buildDir) { - Task verifyIncludes = project.tasks.create("${name}VerifyIncludes") - verifyIncludes.description = "Verifies the includes in the ${name} sample" - verifyIncludes << { + Task verifyIncludesTask = project.tasks.create("${name}VerifyIncludes") + verifyIncludesTask.description = "Verifies the includes in the ${name} sample" + verifyIncludesTask << { Map unprocessedIncludes = [:] buildDir.eachFileRecurse { file -> if (file.name.endsWith('.html')) { @@ -132,6 +144,6 @@ public class SampleBuildConfigurer { throw new GradleException(message.toString()) } } - return verifyIncludes + return verifyIncludesTask } } \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 485dec89a..67d7dc178 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -16,21 +16,26 @@ If you want to jump straight in, a number of sample applications are available: | {samples}/rest-assured[REST Assured] | Gradle -| Demonstrates the use of Spring REST Docs with REST Assured. +| Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. + +| {samples}/rest-notes-slate[Slate] +| Gradle +| Demonstrates the use of Spring REST Docs with Markdown and + http://github.com/tripit/slate[Slate]. | {samples}/rest-notes-spring-data-rest[Spring Data REST] | Maven | Demonstrates the creation of a getting started guide and an API guide for a service - implemented using Spring Data REST. + implemented using http://projects.spring.io/spring-data-rest/[Spring Data REST]. | {samples}/rest-notes-spring-hateoas[Spring HATEOAS] | Gradle | Demonstrates the creation of a getting started guide and an API guide for a service - implemented using Spring HATEOAS. + implemented using http://projects.spring.io/spring-hateoas/[Spring HATEOAS]. | {samples}/testng[TestNG] | Gradle -| Demonstrates the use of Spring REST Docs with TestNG. +| Demonstrates the use of Spring REST Docs with http://testng.org[TestNG]. |=== diff --git a/samples/rest-notes-slate/README.md b/samples/rest-notes-slate/README.md new file mode 100644 index 000000000..bb4de692c --- /dev/null +++ b/samples/rest-notes-slate/README.md @@ -0,0 +1,24 @@ +# REST Notes Slate + +This sample shows how to document a RESTful API using Spring REST Docs and [Slate][1]. +The sample can be built with Gradle, but requires Ruby and the `bundler` gem to +be installed. + +## Quickstart + +``` +./gradlew build +open build/docs/api-guide.html +``` + +## Details + +The bulk of the documentation is written in Markdown in the file named +[slate/api-guide.md.erb][2]. When the documentation is built, snippets generated by +Spring REST Docs in the [ApiDocumentation][3] tests are incorporated into this +documentation by [ERB][4]. The combined Markdown document is then turned into HTML. + +[1]: https://github.com/tripit/slate +[2]: slate/api-guide.md.erb +[3]: src/test/java/com/example/notes/ApiDocumentation.java +[4]: http://ruby-doc.org/stdlib-2.2.3/libdoc/erb/rdoc/ERB.html \ No newline at end of file diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle new file mode 100644 index 000000000..79788c3d6 --- /dev/null +++ b/samples/rest-notes-slate/build.gradle @@ -0,0 +1,64 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + } +} + +apply plugin: 'java' +apply plugin: 'spring-boot' +apply plugin: 'eclipse' + +repositories { + mavenLocal() + maven { url 'https://repo.spring.io/libs-snapshot' } + mavenCentral() +} + +group = 'com.example' + +sourceCompatibility = 1.7 +targetCompatibility = 1.7 + +ext { + snippetsDir = file('build/generated-snippets') + springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' +} + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-data-jpa' + compile 'org.springframework.boot:spring-boot-starter-data-rest' + + runtime 'com.h2database:h2' + runtime 'org.atteo:evo-inflector:1.2.1' + + testCompile 'com.jayway.jsonpath:json-path' + testCompile 'org.springframework.boot:spring-boot-starter-test' + testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" +} + +test { + outputs.dir snippetsDir +} + +task(bundleInstall, type: Exec) { + workingDir file('slate') + executable 'bundle' + args 'install' +} + +task(slate, type: Exec) { + dependsOn 'bundleInstall', 'test' + workingDir file('slate') + executable 'bundle' + args 'exec', 'middleman', 'build' +} + +build { + dependsOn 'slate' +} + +eclipseJdt.onlyIf { false } +cleanEclipseJdt.onlyIf { false } diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2 GIT binary patch literal 53638 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%0AB1q=av0)K=-*!|(RDoglJ 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/rest-notes-slate/gradlew.bat b/samples/rest-notes-slate/gradlew.bat new file mode 100644 index 000000000..8a0b282aa --- /dev/null +++ b/samples/rest-notes-slate/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/rest-notes-slate/slate/.gitignore b/samples/rest-notes-slate/slate/.gitignore new file mode 100644 index 000000000..f6fc8c00b --- /dev/null +++ b/samples/rest-notes-slate/slate/.gitignore @@ -0,0 +1,22 @@ +*.gem +*.rbc +.bundle +.config +coverage +InstalledFiles +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp +*.DS_STORE +build/ +.cache + +# YARD artifacts +.yardoc +_yardoc +doc/ +.idea/ \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/CHANGELOG.md b/samples/rest-notes-slate/slate/CHANGELOG.md new file mode 100644 index 000000000..ee496db6c --- /dev/null +++ b/samples/rest-notes-slate/slate/CHANGELOG.md @@ -0,0 +1,49 @@ +# Changelog + +## Version 1.2 + +*June 20, 2015* + +**Fixes:** + +- Remove crash on invalid languages +- Update Tocify to scroll to the highlighted header in the Table of Contents +- Fix variable leak and update search algorithms +- Update Python examples to be valid Python +- Update gems +- More misc. bugfixes of Javascript errors +- Add Dockerfile +- Remove unused gems +- Optimize images, fonts, and generated asset files +- Add chinese font support +- Remove RedCarpet header ID patch +- Update language tabs to not disturb existing query strings + +## Version 1.1 + +*July 27th, 2014* + +**Fixes:** + +- Finally, a fix for the redcarpet upgrade bug + +## Version 1.0 + +*July 2, 2014* + +[View Issues](https://github.com/tripit/slate/issues?milestone=1&state=closed) + +**Features:** + +- Responsive designs for phones and tablets +- Started tagging versions + +**Fixes:** + +- Fixed 'unrecognized expression' error +- Fixed #undefined hash bug +- Fixed bug where the current language tab would be unselected +- Fixed bug where tocify wouldn't highlight the current section while searching +- Fixed bug where ids of header tags would have special characters that caused problems +- Updated layout so that pages with disabled search wouldn't load search.js +- Cleaned up Javascript diff --git a/samples/rest-notes-slate/slate/Gemfile b/samples/rest-notes-slate/slate/Gemfile new file mode 100644 index 000000000..0933b9d68 --- /dev/null +++ b/samples/rest-notes-slate/slate/Gemfile @@ -0,0 +1,12 @@ +source 'https://rubygems.org' + +# Middleman +gem 'middleman', '~>3.3.10' +gem 'middleman-gh-pages', '~> 0.0.3' +gem 'middleman-syntax', '~> 2.0.0' +gem 'middleman-autoprefixer', '~> 2.4.4' +gem 'rouge', '~> 1.9.0' +gem 'redcarpet', '~> 3.3.2' + +gem 'rake', '~> 10.4.2' +gem 'therubyracer', '~> 0.12.1', platforms: :ruby diff --git a/samples/rest-notes-slate/slate/Gemfile.lock b/samples/rest-notes-slate/slate/Gemfile.lock new file mode 100644 index 000000000..fff5ee10c --- /dev/null +++ b/samples/rest-notes-slate/slate/Gemfile.lock @@ -0,0 +1,140 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (4.1.11) + i18n (~> 0.6, >= 0.6.9) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.1) + tzinfo (~> 1.1) + autoprefixer-rails (5.2.0.1) + execjs + json + celluloid (0.16.0) + timers (~> 4.0.0) + chunky_png (1.3.4) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.9.1.1) + compass (1.0.3) + chunky_png (~> 1.2) + compass-core (~> 1.0.2) + compass-import-once (~> 1.0.5) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + sass (>= 3.3.13, < 3.5) + compass-core (1.0.3) + multi_json (~> 1.0) + sass (>= 3.3.0, < 3.5) + compass-import-once (1.0.5) + sass (>= 3.2, < 3.5) + erubis (2.7.0) + execjs (2.5.2) + ffi (1.9.8) + haml (4.0.6) + tilt + hike (1.2.3) + hitimes (1.2.2) + hooks (0.4.0) + uber (~> 0.0.4) + i18n (0.7.0) + json (1.8.3) + kramdown (1.7.0) + libv8 (3.16.14.7) + listen (2.10.1) + celluloid (~> 0.16.0) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + middleman (3.3.12) + coffee-script (~> 2.2) + compass (>= 1.0.0, < 2.0.0) + compass-import-once (= 1.0.5) + execjs (~> 2.0) + haml (>= 4.0.5) + kramdown (~> 1.2) + middleman-core (= 3.3.12) + middleman-sprockets (>= 3.1.2) + sass (>= 3.4.0, < 4.0) + uglifier (~> 2.5) + middleman-autoprefixer (2.4.4) + autoprefixer-rails (~> 5.2.0) + middleman-core (>= 3.3.3) + middleman-core (3.3.12) + activesupport (~> 4.1.0) + bundler (~> 1.1) + erubis + hooks (~> 0.3) + i18n (~> 0.7.0) + listen (>= 2.7.9, < 3.0) + padrino-helpers (~> 0.12.3) + rack (>= 1.4.5, < 2.0) + rack-test (~> 0.6.2) + thor (>= 0.15.2, < 2.0) + tilt (~> 1.4.1, < 2.0) + middleman-gh-pages (0.0.3) + rake (> 0.9.3) + middleman-sprockets (3.4.2) + middleman-core (>= 3.3) + sprockets (~> 2.12.1) + sprockets-helpers (~> 1.1.0) + sprockets-sass (~> 1.3.0) + middleman-syntax (2.0.0) + middleman-core (~> 3.2) + rouge (~> 1.0) + minitest (5.7.0) + multi_json (1.11.1) + padrino-helpers (0.12.5) + i18n (~> 0.6, >= 0.6.7) + padrino-support (= 0.12.5) + tilt (~> 1.4.1) + padrino-support (0.12.5) + activesupport (>= 3.1) + rack (1.6.4) + rack-test (0.6.3) + rack (>= 1.0) + rake (10.4.2) + rb-fsevent (0.9.5) + rb-inotify (0.9.5) + ffi (>= 0.5.0) + redcarpet (3.3.2) + ref (1.0.5) + rouge (1.9.0) + sass (3.4.14) + sprockets (2.12.3) + hike (~> 1.2) + multi_json (~> 1.0) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + sprockets-helpers (1.1.0) + sprockets (~> 2.0) + sprockets-sass (1.3.1) + sprockets (~> 2.0) + tilt (~> 1.1) + therubyracer (0.12.2) + libv8 (~> 3.16.14.0) + ref + thor (0.19.1) + thread_safe (0.3.5) + tilt (1.4.1) + timers (4.0.1) + hitimes + tzinfo (1.2.2) + thread_safe (~> 0.1) + uber (0.0.13) + uglifier (2.7.1) + execjs (>= 0.3.0) + json (>= 1.8.0) + +PLATFORMS + ruby + +DEPENDENCIES + middleman (~> 3.3.10) + middleman-autoprefixer (~> 2.4.4) + middleman-gh-pages (~> 0.0.3) + middleman-syntax (~> 2.0.0) + rake (~> 10.4.2) + redcarpet (~> 3.3.2) + rouge (~> 1.9.0) + therubyracer (~> 0.12.1) diff --git a/samples/rest-notes-slate/slate/LICENSE b/samples/rest-notes-slate/slate/LICENSE new file mode 100644 index 000000000..5ceddf59f --- /dev/null +++ b/samples/rest-notes-slate/slate/LICENSE @@ -0,0 +1,13 @@ +Copyright 2008-2013 Concur Technologies, Inc. + +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. \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/README.md b/samples/rest-notes-slate/slate/README.md new file mode 100644 index 000000000..2f042e7f2 --- /dev/null +++ b/samples/rest-notes-slate/slate/README.md @@ -0,0 +1,128 @@ +Slate +======== + +[![Build Status](https://travis-ci.org/tripit/slate.svg?branch=master)](https://travis-ci.org/tripit/slate) [![Dependency Status](https://gemnasium.com/tripit/slate.png)](https://gemnasium.com/tripit/slate) + +Slate helps you create beautiful API documentation. Think of it as an intelligent, responsive documentation template for your API. + +Screenshot of Example Documentation created with Slate + +*The example above was created with Slate. Check it out at [tripit.github.io/slate](http://tripit.github.io/slate).* + +Features +------------ + +* **Clean, intuitive design** — with Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [Paypal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even print. + +* **Everything on a single page** — gone are the days where your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy. + +* **Slate is just Markdown** — when you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown — even the code samples are just Markdown code blocks! + +* **Write code samples in multiple languages** — if your API has bindings in multiple programming languages, you easily put in tabs to switch between them. In your document, you'll distinguish different languages by specifying the language name at the top of each code block, just like with Github Flavored Markdown! + +* **Out-of-the-box syntax highlighting** for [almost 60 languages](http://rouge.jayferd.us/demo), no configuration required. + +* **Automatic, smoothly scrolling table of contents** on the far left of the page. As you scroll, it displays your current position in the document. It's fast, too. We're using Slate at TripIt to build documentation for our new API, where our table of contents has over 180 entries. We've made sure that the performance remains excellent, even for larger documents. + +* **Let your users update your documentation for you** — by default, your Slate-generated documentation is hosted in a public Github repository. Not only does this mean you get free hosting for your docs with Github Pages, but it also makes it's simple for other developers to make pull requests to your docs if they find typos or other problems. Of course, if you don't want to, you're welcome to not use Github and host your docs elsewhere! + +Getting starting with Slate is super easy! Simply fork this repository, and then follow the instructions below. Or, if you'd like to check out what Slate is capable of, take a look at the [sample docs](http://tripit.github.io/slate). + + + +Getting Started with Slate +------------------------------ + +### Prerequisites + +You're going to need: + + - **Linux or OS X** — Windows may work, but is unsupported. + - **Ruby, version 1.9.3 or newer** + - **Bundler** — If Ruby is already installed, but the `bundle` command doesn't work, just run `gem install bundler` in a terminal. + +### Getting Set Up + + 1. Fork this repository on Github. + 2. Clone *your forked repository* (not our original one) to your hard drive with `git clone https://github.com/YOURUSERNAME/slate.git` + 3. `cd slate` + 4. Install all dependencies: `bundle install` + 5. Start the test server: `bundle exec middleman server` + +Or use the included Dockerfile! (must install Docker first) + +```shell +docker build -t slate . +docker run -d -p 4567:4567 --name slate -v $(pwd)/source:/app/source slate +``` + +You can now see the docs at . Whoa! That was fast! + +*Note: if you're using the Docker setup on OSX, the docs will be +availalable at the output of `boot2docker ip` instead of `localhost:4567`.* + +Now that Slate is all set up your machine, you'll probably want to learn more about [editing Slate markdown](https://github.com/tripit/slate/wiki/Markdown-Syntax), or [how to publish your docs](https://github.com/tripit/slate/wiki/Deploying-Slate). + +Examples of Slate in the Wild +--------------------------------- + +* [Travis-CI's API docs](http://docs.travis-ci.com/api/) +* [Mozilla's localForage docs](http://mozilla.github.io/localForage/) +* [Mozilla Recroom](http://mozilla.github.io/recroom/) +* [ChaiOne Gameplan API docs](http://chaione.github.io/gameplanb2b/#introduction) +* [Drcaban's Build a Quine tutorial](http://drcabana.github.io/build-a-quine/#introduction) +* [PricePlow API docs](https://www.priceplow.com/api/documentation) +* [Emerging Threats API docs](http://apidocs.emergingthreats.net/) +* [Appium docs](http://appium.io/slate/en/master) +* [Golazon Developer](http://developer.golazon.com) +* [Dwolla API docs](https://docs.dwolla.com/) +* [RozpisyZapasu API docs](http://www.rozpisyzapasu.cz/dev/api/) +* [Codestar Framework Docs](http://codestarframework.com/documentation/) +* [Buddycloud API](http://buddycloud.com/api) +* [Crafty Clicks API](https://craftyclicks.co.uk/api/) +* [Paracel API Reference](http://paracel.io/docs/api_reference.html) +* [Switch Payments Documentation](http://switchpayments.com/docs/) & [API](http://switchpayments.com/developers/) +* [Coinbase API Reference](https://developers.coinbase.com/api) +* [Whispir.io API](https://whispir.github.io/api) +* [NASA API](https://data.nasa.gov/developer/external/planetary/) +* [CardPay API](https://developers.cardpay.com/) +* [IBM Cloudant](https://docs-testb.cloudant.com/content-review/_design/couchapp/index.html) +* [Bitrix basis components](http://bbc.bitrix.expert/) +* [viagogo API Documentation](http://developer.viagogo.net/) +* [Fidor Bank API Documentation](http://docs.fidor.de/) +* [Market Prophit API Documentation](http://developer.marketprophit.com/) +* [OAuth.io API Documentation](http://docs.oauth.io/) +* [Aircall for Developers](http://developer.aircall.io/) +* [SupportKit API Docs](http://docs.supportkit.io/) +* [SocialRadar's LocationKit Docs](https://docs.locationkit.io/) +* [SafetyCulture API Documentation](https://developer.safetyculture.io/) +* [hosting.de API Documentation](https://www.hosting.de/docs/api/) + +(Feel free to add your site to this list in a pull request!) + +Need Help? Found a bug? +-------------------- + +Just [submit a issue](https://github.com/tripit/slate/issues) to the Slate Github if you need any help. And, of course, feel free to submit pull requests with bug fixes or changes. + + +Contributors +-------------------- + +Slate was built by [Robert Lord](https://lord.io) while at [TripIt](http://tripit.com). + +Thanks to the following people who have submitted major pull requests: + +- [@chrissrogers](https://github.com/chrissrogers) +- [@bootstraponline](https://github.com/bootstraponline) +- [@realityking](https://github.com/realityking) + +Also, thanks to [Sauce Labs](http://saucelabs.com) for helping sponsor the project. + +Special Thanks +-------------------- +- [Middleman](https://github.com/middleman/middleman) +- [jquery.tocify.js](https://github.com/gfranko/jquery.tocify.js) +- [middleman-syntax](https://github.com/middleman/middleman-syntax) +- [middleman-gh-pages](https://github.com/neo/middleman-gh-pages) +- [Font Awesome](http://fortawesome.github.io/Font-Awesome/) diff --git a/samples/rest-notes-slate/slate/Rakefile b/samples/rest-notes-slate/slate/Rakefile new file mode 100644 index 000000000..6a952e1e9 --- /dev/null +++ b/samples/rest-notes-slate/slate/Rakefile @@ -0,0 +1,6 @@ +require 'middleman-gh-pages' +require 'rake/clean' + +CLOBBER.include('build') + +task :default => [:build] diff --git a/samples/rest-notes-slate/slate/config.rb b/samples/rest-notes-slate/slate/config.rb new file mode 100644 index 000000000..fdcb21f32 --- /dev/null +++ b/samples/rest-notes-slate/slate/config.rb @@ -0,0 +1,41 @@ +# Markdown +set :markdown_engine, :redcarpet +set :markdown, + fenced_code_blocks: true, + smartypants: true, + disable_indented_code_blocks: true, + prettify: true, + tables: true, + with_toc_data: true, + no_intra_emphasis: true + +# Assets +set :css_dir, 'stylesheets' +set :js_dir, 'javascripts' +set :images_dir, 'images' +set :fonts_dir, 'fonts' + +# Activate the syntax highlighter +activate :syntax + +activate :autoprefixer do |config| + config.browsers = ['last 2 version', 'Firefox ESR'] + config.cascade = false + config.inline = true +end + +# Github pages require relative links +activate :relative_assets +set :relative_links, true + +# Build Configuration + +set :build_dir, '../build/docs' + +configure :build do + activate :minify_css + activate :minify_javascript + # activate :relative_assets + # activate :asset_hash + # activate :gzip +end diff --git a/samples/rest-notes-slate/slate/font-selection.json b/samples/rest-notes-slate/slate/font-selection.json new file mode 100755 index 000000000..5e78f5d86 --- /dev/null +++ b/samples/rest-notes-slate/slate/font-selection.json @@ -0,0 +1,148 @@ +{ + "IcoMoonType": "selection", + "icons": [ + { + "icon": { + "paths": [ + "M438.857 73.143q119.429 0 220.286 58.857t159.714 159.714 58.857 220.286-58.857 220.286-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857zM512 785.714v-108.571q0-8-5.143-13.429t-12.571-5.429h-109.714q-7.429 0-13.143 5.714t-5.714 13.143v108.571q0 7.429 5.714 13.143t13.143 5.714h109.714q7.429 0 12.571-5.429t5.143-13.429zM510.857 589.143l10.286-354.857q0-6.857-5.714-10.286-5.714-4.571-13.714-4.571h-125.714q-8 0-13.714 4.571-5.714 3.429-5.714 10.286l9.714 354.857q0 5.714 5.714 10t13.714 4.286h105.714q8 0 13.429-4.286t6-10z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "exclamation-circle" + ], + "defaultCode": 61546, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 100, + "order": 4, + "prevSize": 28, + "code": 58880, + "name": "exclamation-sign", + "ligatures": "" + }, + "setIdx": 0, + "iconIdx": 0 + }, + { + "icon": { + "paths": [ + "M585.143 786.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-54.857v-292.571q0-8-5.143-13.143t-13.143-5.143h-182.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h54.857v182.857h-54.857q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h256q8 0 13.143-5.143t5.143-13.143zM512 274.286v-91.429q0-8-5.143-13.143t-13.143-5.143h-109.714q-8 0-13.143 5.143t-5.143 13.143v91.429q0 8 5.143 13.143t13.143 5.143h109.714q8 0 13.143-5.143t5.143-13.143zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "info-circle" + ], + "defaultCode": 61530, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 85, + "order": 3, + "name": "info-sign", + "prevSize": 28, + "code": 58882 + }, + "setIdx": 0, + "iconIdx": 2 + }, + { + "icon": { + "paths": [ + "M733.714 419.429q0-16-10.286-26.286l-52-51.429q-10.857-10.857-25.714-10.857t-25.714 10.857l-233.143 232.571-129.143-129.143q-10.857-10.857-25.714-10.857t-25.714 10.857l-52 51.429q-10.286 10.286-10.286 26.286 0 15.429 10.286 25.714l206.857 206.857q10.857 10.857 25.714 10.857 15.429 0 26.286-10.857l310.286-310.286q10.286-10.286 10.286-25.714zM877.714 512q0 119.429-58.857 220.286t-159.714 159.714-220.286 58.857-220.286-58.857-159.714-159.714-58.857-220.286 58.857-220.286 159.714-159.714 220.286-58.857 220.286 58.857 159.714 159.714 58.857 220.286z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "check-circle" + ], + "defaultCode": 61528, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 83, + "order": 9, + "prevSize": 28, + "code": 58886, + "name": "ok-sign" + }, + "setIdx": 0, + "iconIdx": 6 + }, + { + "icon": { + "paths": [ + "M658.286 475.429q0-105.714-75.143-180.857t-180.857-75.143-180.857 75.143-75.143 180.857 75.143 180.857 180.857 75.143 180.857-75.143 75.143-180.857zM950.857 950.857q0 29.714-21.714 51.429t-51.429 21.714q-30.857 0-51.429-21.714l-196-195.429q-102.286 70.857-228 70.857-81.714 0-156.286-31.714t-128.571-85.714-85.714-128.571-31.714-156.286 31.714-156.286 85.714-128.571 128.571-85.714 156.286-31.714 156.286 31.714 128.571 85.714 85.714 128.571 31.714 156.286q0 125.714-70.857 228l196 196q21.143 21.143 21.143 51.429z" + ], + "width": 951, + "attrs": [], + "isMulticolor": false, + "tags": [ + "search" + ], + "defaultCode": 61442, + "grid": 14 + }, + "attrs": [], + "properties": { + "id": 2, + "order": 1, + "prevSize": 28, + "code": 58887, + "name": "icon-search" + }, + "setIdx": 0, + "iconIdx": 7 + } + ], + "height": 1024, + "metadata": { + "name": "slate", + "license": "SIL OFL 1.1" + }, + "preferences": { + "showGlyphs": true, + "showQuickUse": true, + "showQuickUse2": true, + "showSVGs": true, + "fontPref": { + "prefix": "icon-", + "metadata": { + "fontFamily": "slate", + "majorVersion": 1, + "minorVersion": 0, + "description": "Based on FontAwesome", + "license": "SIL OFL 1.1" + }, + "metrics": { + "emSize": 1024, + "baseline": 6.25, + "whitespace": 50 + }, + "resetPoint": 58880, + "showSelector": false, + "selector": "class", + "classSelector": ".icon", + "showMetrics": false, + "showMetadata": true, + "showVersion": true, + "ie7": false + }, + "imagePref": { + "prefix": "icon-", + "png": true, + "useClassSelector": true, + "color": 4473924, + "bgColor": 16777215 + }, + "historySize": 100, + "showCodes": true, + "gridSize": 16, + "showLiga": false + } +} diff --git a/samples/rest-notes-slate/slate/source/api-guide.md.erb b/samples/rest-notes-slate/slate/source/api-guide.md.erb new file mode 100644 index 000000000..11248a3ea --- /dev/null +++ b/samples/rest-notes-slate/slate/source/api-guide.md.erb @@ -0,0 +1,206 @@ +--- +title: REST Notes API Guide + +language_tabs: + - shell + - http + +search: true +--- + +# Overview + + +## HTTP verbs + +RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP verbs. + + +Verb | Usage +-------- | ----- +`GET` | Used to retrieve a resource +`POST` | Used to create a new resource +`PATCH` | Used to update an existing resource, including partial updates +`DELETE` | Used to delete an existing resource + +## HTTP status codes + +RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP status codes. + +Status code | Usage +----------------- | ----- +`200 OK` | The request completed successfully +`201 Created` | A new resource has been created successfully. The resource's URI is available from the response's `Location` header +`204 No Content` | An update to an existing resource has been applied successfully +`400 Bad Request` | The request was malformed. The response body will include an error providing further information +`404 Not Found` | The requested resource did not exist + +## Errors + +<%= ERB.new(File.read("../build/generated-snippets/error-example/http-response.md")).result(binding) %> + +Whenever an error response (status code >= 400) is returned, the body will contain a JSON object +that describes the problem. The error object has the following structure: + +<%= ERB.new(File.read("../build/generated-snippets/error-example/response-fields.md")).result(binding) %> + +## Hypermedia + +RESTful Notes uses hypermedia and resources include links to other resources in their +responses. Responses are in [Hypertext Application Language (HAL)](http://stateless.co/hal_specification.html format). +Links can be found beneath the `_links` key. Users of the API should not create URIs +themselves, instead they should use the above-described links to navigate + + + +# Resources + + + +## Index + +<%= ERB.new(File.read("../build/generated-snippets/index-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/index-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/index-example/http-response.md")).result(binding) %> + +The index provides the entry point into the service. A `GET` request is used to access the index. + +### Response structure + +<%= ERB.new(File.read("../build/generated-snippets/index-example/response-fields.md")).result(binding) %> + +### Links + +<%= ERB.new(File.read("../build/generated-snippets/index-example/links.md")).result(binding) %> + + + +## Notes + +The Notes resources is used to create and list notes + +### Listing notes + +<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/http-response.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/curl-request.md")).result(binding) %> + +A `GET` request will list all of the service's notes. + +#### Response structure + +<%= ERB.new(File.read("../build/generated-snippets/notes-list-example/response-fields.md")).result(binding) %> + +### Creating a note + +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-response.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/curl-request.md")).result(binding) %> + +A `POST` request is used to create a note + +#### Request structure + +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/request-fields.md")).result(binding) %> + + + +## Tags + +The Tags resource is used to create and list tags. + +### Listing tags + +<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/http-response.md")).result(binding) %> + +A `GET` request will list all of the service's tags. + +#### Response structure + +<%= ERB.new(File.read("../build/generated-snippets/tags-list-example/response-fields.md")).result(binding) %> + + +### Creating a tag + +<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/notes-create-example/http-response.md")).result(binding) %> + +A `POST` request is used to create a tag + +#### Request structure + +<%= ERB.new(File.read("../build/generated-snippets/tags-create-example/request-fields.md")).result(binding) %> + + + +## Note + +The Note resource is used to retrieve, update, and delete individual notes + +### Retrieve a note + +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/http-response.md")).result(binding) %> + +A `GET` request will retrieve the details of a note + +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/response-fields.md")).result(binding) %> + +#### Links + +<%= ERB.new(File.read("../build/generated-snippets/note-get-example/links.md")).result(binding) %> + +### Update a note + +<%= ERB.new(File.read("../build/generated-snippets/note-update-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/note-update-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/note-update-example/http-response.md")).result(binding) %> + +A `PATCH` request is used to update a note + +#### Request structure + +<%= ERB.new(File.read("../build/generated-snippets/note-update-example/request-fields.md")).result(binding) %> + +To leave an attribute of a note unchanged, any of the above may be omitted from the +request. + + + +## Tag + +The Tag resource is used to retrieve, update, and delete individual tags + +### Retrieve a tag + +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/http-response.md")).result(binding) %> + +A `GET` request will retrieve the details of a tag + +#### Response structure + +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/response-fields.md")).result(binding) %> + +#### Links + +<%= ERB.new(File.read("../build/generated-snippets/tag-get-example/links.md")).result(binding) %> + +### Update a tag + +<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/curl-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/http-request.md")).result(binding) %> +<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/http-response.md")).result(binding) %> + +A `PATCH` request is used to update a tag + +#### Request structure + +<%= ERB.new(File.read("../build/generated-snippets/tag-update-example/request-fields.md")).result(binding) %> \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.eot b/samples/rest-notes-slate/slate/source/fonts/slate.eot new file mode 100755 index 0000000000000000000000000000000000000000..13c4839a1975d4c92d66753d75553f922743c6ef GIT binary patch literal 1876 zcmaJ?&2Jl35TEDWeRg&sP8)xqh$Kr)Yzaiu#`dnNaHtS!Xx9i53LF}#nAmmP`Xfq= z;s{Zte*qCEK;i~h4hYFPaA-x4sD#vVrBXQ{MIaD@1Ke8kn|8tnQ^fdI9N^x@? zd<=RUJW{D`U;b-v<0kYSA}zO8E|&DaCqF0BzlPme0}$TUZbAP8`m<~GR{K~75*dg= zcCEQu4DE@ZpmS=+>&5muJwb0nf0^x#V!izR7X1vpggLV7&CM3_1j&!tPMS_)mkgrN zC!rsJe5kniow8zt{RT+zltXle=pd}!=-!|+8X9a|iyqm&z#GOh#?Z4hMmoI$K1va6 zrUYgm&_U=R+`ZrJ0!LQ9E`42ef0@uHvC8U zo}w4%B?O&MCX$JGEG)w^HIqqa()pb0xK4IFpUb457c*fwDPqaQf|z%md}h1{#>ac0 zD>_@{@&c$_-fEYWRBE3yj7U8q4MTz%hA^cOxdwei%19MlXpM(P_)+e=@Rm}B>tXap zwoqO*DogaoKF+^`!zvX>{f10 zq*F6dk@0&Ok4=k2cHR|EKKaOn+_Q3KG-~~J-9n!;&D+d{W9=SQ|IqlDm9?y2uUl{( zi(0o$Q@Cby<g+B7U zE7gMM6{=S}Ps}C~lhg72%h#4X&vB-0d)je4Z)w>(?>M%-xE~jH;l@L}Lcyy(n4O9z z65lS`w&R>XbW_^$2bKN!lz(S%N3N!tcP>R={D&-^3rjyfS-aWi!AkH>sk+00G5&qW zC1%n(1Gmpd;5$IPUF_Lw@K%&WbbxEX?LgKcF9x!K$J`8LiMQ^#KsG5yOZ=|rBS1K&l2uG4tC&hwF_o-hDp_Le zTrgI}?0+pse + + +Generated by IcoMoon + + + + + + + + + + diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.ttf b/samples/rest-notes-slate/slate/source/fonts/slate.ttf new file mode 100755 index 0000000000000000000000000000000000000000..ace9a46a7e1ed6b6ab3de2f30ef3e27c572f88d3 GIT binary patch literal 1720 zcmaJ>-D_KA7=PaL@t*V?ZMro%kz(6pV-rwZyCyk@Q7?wb>UJv9`gO6wj7^qiCTUqB zO%X@;7dX69!8^V6LWJzXiw!F%45qgVdLa}=5xvm6E&iVKo-9$jJ@CHI_viaTf`}a2 zC!H2wcyVDVd0amU$>&(FZ8pn0bm7yth{U7dH)`ef4)6r{E^wmO*t_`0^~*QG?-S|8 zt!lYq{5ky*k?|Sy{uTt*p8hrX-@re<)$DYS^+1t{800m!H_O^}@g4X@@W-3w?hZXf zuY!M;^{sNV`qeJ|2)=?Gg`Mqo2XzAEd#oqjAaRXMBJF+c79{T|EPkbe7-PE;5S;Q~ zaGL1Q(r@%{&}khDI-bPX!~GUGz-0@x9Aaiik?BxrHq?#(PnWnI%nYaReOv*$ZSm>?)ctla|1hAG;T1^YPnfOv{N&_J8ekcvoG^Cm?MLpzb znO-8ASEU{s)D{<<&JKz%JjTWAM|EWWzjHMajT;ECdZ?bsKw&|^tZONrkvOuIkI%De zUYTtG_26}0jYK0>4*B1QgBuPLXU?}t^*TiboK|r`R>P0_HD+(cdi{Ze{FKYDLBs0R~?v6B%Rx~Edo2aasT@IJ-vtfG(iE^ z$Awv_E5l{^C4s84a|;3+->t#z!u^V-9Nj!@+Ph(RslFP9tMyA^DCS*vdNzG<@yc2l z`u?ov&H8>AuC0gXeBbj{4$|U#n6XQ^x*FE+&d;P>_lp(J^Zj%8%oMl&cI_ZN6TKO{ zkvFp2-&{yO{TDd~50<`txN&oc<4*8TskuV~pXj~g5i{t$k=GYVU^@bQTx>a5uvcK? zADE$i`dge4A3((KH9;?{zv*7K*f>Jt^humckR5yQeHf=xv0R7Ti)jP&N=%#Ng5wPM z_VCv|5z{smX^sCCV+<0Gsc02b(JH2*RZK;zn2MJ0U5I^E%r-TsOdYDyD_EDQO?BF) z7OFc*Cuk9TtZz5Uo$8RKb(q)a%}C8|gD__z_YMNoV9|<#sst6tUZ*|mXK52w&tq|_ J6Wq-M;U6NW`v(93 literal 0 HcmV?d00001 diff --git a/samples/rest-notes-slate/slate/source/fonts/slate.woff b/samples/rest-notes-slate/slate/source/fonts/slate.woff new file mode 100755 index 0000000000000000000000000000000000000000..1e72e0ee0018119d7c814c2097cc60c0bcd05d84 GIT binary patch literal 1796 zcmaJ>&2Jl35TCanZ=GF;-Nqj%Qj#SmwgjS~vAs(b4i$%lc7v*fuR|jhlemtH?Nqf< z5&;$c3y8RZ#0{p83^QtGf$~^ZU#vdPL3b z4fcBr_DN>rKz!G#?&4f~pM4ZRM6a}~ts1aTadwIM%N_dh>UO7#K7YRFXF@YA68l_? z@xFm7>0K?wZ&VUvut!OxMlIIQ5*<0_&Hha~Yl49Y@PK@!7+CqFG*;eClSR)#j$=Xw zNnrjF9T`VX|4zRT99<||DqHk_nzSa(NzO5voBad{L?lOWoE4r?ZbRP(V_X@TZL>{} z(3A8mk}l-3xojrwNJr*pi-lsLQVxSKC{0w##ljO}){#>poy#tYg)pcTCk9|L<3To?f*omEO$b9ODUa}gVj!a zAvUB1l6OCpmTg;7PgnO)phbF-Xik@UVo+OLa3((}zVs*;Zywi?{r}GOL=0{q1ou!q ztD%;HAbGE?Z5HC#RzAMSTXWiN9ioS*i+Usm@#fI}eK@$`F!8DQHtAj`iEnm!UKH}P zNl{d*%%o>TwzLq6ppv_9BR_a$H<|Q)z2RXkyY6k4BJlQ)o4+xU@=Bif%MA~%sib$? zbw%hV*Y96nzi0MvpHdWZeO#D>x^i4rP!XsqKRYk5@ZB2RF5E9QWp(qg81F^VmBvaG ztu(Ggk(kS7r)DyTm#?ozQ4q}d{!9==(dt@sJ_vk&`7k5ChZ~1PD=Sgs?%Z@HoBe#* z_k-a4JvVKwyWWZ=q2@1#9uk~9EfrHBtA=!8%MC` zIGu@c6SyDi7WCym@ z2-w*rE$c_UfviKEL=Vy>={0bo+(i5#P1q3$6VB``hVMrCzU@B6vbqKcPy}m-BtQn- zkylU>byNe6DE>18g5d9bm%bZqa|qF=7&3(|U!AY)%Yg-vOsqKIq$w0D$r*3}ks4=+ z>U6?w2xMd=B$;zQ9v)F1K7KwfQ9|)<9nl&i?6-rchs{I8LC1eK^@#(|^QwB-{VeQz zoHLn@cwG?Yo(X!F(Q8iBOqSNs${h}C*?qJYLuZAnVXqx{ww|}Ux0`ca^V#hz?c7{r z7ldYbmk!M1m3G+IYC6(!VcCzqp&pJkZ#1@kw2W<^EJMqOM;^&dq6ELC*|pK!(cC0< zbZtF%BzLsuTw5O*+eg;dX0a+hcfI=gj8a~qWYJoMamHAo)!Qq|6&~9S8`{d0T?L&% z@$R=P6H0rTp6V+-Dlb>EB67JEMQ>kk%`y}{hvBC(RNEw)*%|U|4>SL+B=3rE@y*rw zR_bLZr&~XN?C;+yx2IilcDnrb?M)}An;DsTe@^rq$9L-UWNqY_oTyWyYisN3CMIh; z*C)b12exYgK^2c4S}J#2YJ+00rUlj%+LqQ7E%lldEc`K|1Bky)iLkwF{Wltvj!{A+PBEzhE3BX) zFBgqmj@&z?N1*=OK?z54T7is8FiA(N66oe`X+cKl>`n&&000000HC;3 zk^ug-#56-JW5JAtMV6Rgj#+|9A(7;@^`+^dWy@Alt7D86w|;R>QMs>d+47HJVfKhH aD;vc&%m&dlj4($-ipx}q&#}A+00017%3vV? literal 0 HcmV?d00001 diff --git a/samples/rest-notes-slate/slate/source/images/logo.png b/samples/rest-notes-slate/slate/source/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fa1f13da8193dacca747df56cc1f7bb0bc111a7e GIT binary patch literal 3507 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 literal 0 HcmV?d00001 diff --git a/samples/rest-notes-slate/slate/source/images/navbar.png b/samples/rest-notes-slate/slate/source/images/navbar.png new file mode 100644 index 0000000000000000000000000000000000000000..df38e90d87e1a215371b4977e18cde90f8832537 GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^3Lwk@BpAX3RW*PVQ%R6tFatx`= 1) { + var language = parseURL(location.search).language + if (language) { + return language; + } else if (jQuery.inArray(location.search.substr(1), languages) != -1) { + return location.search.substr(1); + } + } + + return false; + } + + // returns a new query string with the new language in it + function generateNewQueryString(language) { + var url = parseURL(location.search); + if (url.language) { + url.language = language; + return stringifyURL(url); + } + return language; + } + + // if a button is clicked, add the state to the history + function pushURL(language) { + if (!history) { return; } + var hash = window.location.hash; + if (hash) { + hash = hash.replace(/^#+/, ''); + } + history.pushState({}, '', '?' + generateNewQueryString(language) + '#' + hash); + + // save language as next default + localStorage.setItem("language", language); + } + + function setupLanguages(l) { + var defaultLanguage = localStorage.getItem("language"); + + languages = l; + + var presetLanguage = getLanguageFromQueryString(); + if (presetLanguage) { + // the language is in the URL, so use that language! + activateLanguage(presetLanguage); + + localStorage.setItem("language", presetLanguage); + } else if ((defaultLanguage !== null) && (jQuery.inArray(defaultLanguage, languages) != -1)) { + // the language was the last selected one saved in localstorage, so use that language! + activateLanguage(defaultLanguage); + } else { + // no language selected, so use the default + activateLanguage(languages[0]); + } + } + + // if we click on a language tab, activate that language + $(function() { + $(".lang-selector a").on("click", function() { + var language = $(this).data("language-name"); + pushURL(language); + activateLanguage(language); + return false; + }); + window.onpopstate = function() { + activateLanguage(getLanguageFromQueryString()); + }; + }); +})(window); diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js new file mode 100644 index 000000000..91f38a04e --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js @@ -0,0 +1,74 @@ +//= require ../lib/_lunr +//= require ../lib/_jquery.highlight +(function () { + 'use strict'; + + var content, searchResults; + var highlightOpts = { element: 'span', className: 'search-highlight' }; + + var index = new lunr.Index(); + + index.ref('id'); + index.field('title', { boost: 10 }); + index.field('body'); + index.pipeline.add(lunr.trimmer, lunr.stopWordFilter); + + $(populate); + $(bind); + + function populate() { + $('h1, h2').each(function() { + var title = $(this); + var body = title.nextUntil('h1, h2'); + index.add({ + id: title.prop('id'), + title: title.text(), + body: body.text() + }); + }); + } + + function bind() { + content = $('.content'); + searchResults = $('.search-results'); + + $('#input-search').on('keyup', search); + } + + function search(event) { + unhighlight(); + searchResults.addClass('visible'); + + // ESC clears the field + if (event.keyCode === 27) this.value = ''; + + if (this.value) { + var results = index.search(this.value).filter(function(r) { + return r.score > 0.0001; + }); + + if (results.length) { + searchResults.empty(); + $.each(results, function (index, result) { + var elem = document.getElementById(result.ref); + searchResults.append("

  • " + $(elem).text() + "
  • "); + }); + highlight.call(this); + } else { + searchResults.html('
  • '); + $('.search-results li').text('No Results Found for "' + this.value + '"'); + } + } else { + unhighlight(); + searchResults.removeClass('visible'); + } + } + + function highlight() { + if (this.value) content.highlight(this.value, highlightOpts); + } + + function unhighlight() { + content.unhighlight(highlightOpts); + } +})(); diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js new file mode 100644 index 000000000..bc2aa3e1f --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js @@ -0,0 +1,55 @@ +//= require ../lib/_jquery_ui +//= require ../lib/_jquery.tocify +//= require ../lib/_imagesloaded.min +(function (global) { + 'use strict'; + + var closeToc = function() { + $(".tocify-wrapper").removeClass('open'); + $("#nav-button").removeClass('open'); + }; + + var makeToc = function() { + global.toc = $("#toc").tocify({ + selectors: 'h1, h2', + extendPage: false, + theme: 'none', + smoothScroll: false, + showEffectSpeed: 0, + hideEffectSpeed: 180, + ignoreSelector: '.toc-ignore', + highlightOffset: 60, + scrollTo: -1, + scrollHistory: true, + hashGenerator: function (text, element) { + return element.prop('id'); + } + }).data('toc-tocify'); + + $("#nav-button").click(function() { + $(".tocify-wrapper").toggleClass('open'); + $("#nav-button").toggleClass('open'); + return false; + }); + + $(".page-wrapper").click(closeToc); + $(".tocify-item").click(closeToc); + }; + + // Hack to make already open sections to start opened, + // instead of displaying an ugly animation + function animate() { + setTimeout(function() { + toc.setOption('showEffectSpeed', 180); + }, 50); + } + + $(function() { + makeToc(); + animate(); + $('.content').imagesLoaded( function() { + global.toc.calculateHeights(); + }); + }); +})(window); + diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js new file mode 100644 index 000000000..6798f3c03 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_energize.js @@ -0,0 +1,169 @@ +/** + * energize.js v0.1.0 + * + * Speeds up click events on mobile devices. + * https://github.com/davidcalhoun/energize.js + */ + +(function() { // Sandbox + /** + * Don't add to non-touch devices, which don't need to be sped up + */ + if(!('ontouchstart' in window)) return; + + var lastClick = {}, + isThresholdReached, touchstart, touchmove, touchend, + click, closest; + + /** + * isThresholdReached + * + * Compare touchstart with touchend xy coordinates, + * and only fire simulated click event if the coordinates + * are nearby. (don't want clicking to be confused with a swipe) + */ + isThresholdReached = function(startXY, xy) { + return Math.abs(startXY[0] - xy[0]) > 5 || Math.abs(startXY[1] - xy[1]) > 5; + }; + + /** + * touchstart + * + * Save xy coordinates when the user starts touching the screen + */ + touchstart = function(e) { + this.startXY = [e.touches[0].clientX, e.touches[0].clientY]; + this.threshold = false; + }; + + /** + * touchmove + * + * Check if the user is scrolling past the threshold. + * Have to check here because touchend will not always fire + * on some tested devices (Kindle Fire?) + */ + touchmove = function(e) { + // NOOP if the threshold has already been reached + if(this.threshold) return false; + + this.threshold = isThresholdReached(this.startXY, [e.touches[0].clientX, e.touches[0].clientY]); + }; + + /** + * touchend + * + * If the user didn't scroll past the threshold between + * touchstart and touchend, fire a simulated click. + * + * (This will fire before a native click) + */ + touchend = function(e) { + // Don't fire a click if the user scrolled past the threshold + if(this.threshold || isThresholdReached(this.startXY, [e.changedTouches[0].clientX, e.changedTouches[0].clientY])) { + return; + } + + /** + * Create and fire a click event on the target element + * https://developer.mozilla.org/en/DOM/event.initMouseEvent + */ + var touch = e.changedTouches[0], + evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); + evt.simulated = true; // distinguish from a normal (nonsimulated) click + e.target.dispatchEvent(evt); + }; + + /** + * click + * + * Because we've already fired a click event in touchend, + * we need to listed for all native click events here + * and suppress them as necessary. + */ + click = function(e) { + /** + * Prevent ghost clicks by only allowing clicks we created + * in the click event we fired (look for e.simulated) + */ + var time = Date.now(), + timeDiff = time - lastClick.time, + x = e.clientX, + y = e.clientY, + xyDiff = [Math.abs(lastClick.x - x), Math.abs(lastClick.y - y)], + target = closest(e.target, 'A') || e.target, // needed for standalone apps + nodeName = target.nodeName, + isLink = nodeName === 'A', + standAlone = window.navigator.standalone && isLink && e.target.getAttribute("href"); + + lastClick.time = time; + lastClick.x = x; + lastClick.y = y; + + /** + * Unfortunately Android sometimes fires click events without touch events (seen on Kindle Fire), + * so we have to add more logic to determine the time of the last click. Not perfect... + * + * Older, simpler check: if((!e.simulated) || standAlone) + */ + if((!e.simulated && (timeDiff < 500 || (timeDiff < 1500 && xyDiff[0] < 50 && xyDiff[1] < 50))) || standAlone) { + e.preventDefault(); + e.stopPropagation(); + if(!standAlone) return false; + } + + /** + * Special logic for standalone web apps + * See http://stackoverflow.com/questions/2898740/iphone-safari-web-app-opens-links-in-new-window + */ + if(standAlone) { + window.location = target.getAttribute("href"); + } + + /** + * Add an energize-focus class to the targeted link (mimics :focus behavior) + * TODO: test and/or remove? Does this work? + */ + if(!target || !target.classList) return; + target.classList.add("energize-focus"); + window.setTimeout(function(){ + target.classList.remove("energize-focus"); + }, 150); + }; + + /** + * closest + * @param {HTMLElement} node current node to start searching from. + * @param {string} tagName the (uppercase) name of the tag you're looking for. + * + * Find the closest ancestor tag of a given node. + * + * Starts at node and goes up the DOM tree looking for a + * matching nodeName, continuing until hitting document.body + */ + closest = function(node, tagName){ + var curNode = node; + + while(curNode !== document.body) { // go up the dom until we find the tag we're after + if(!curNode || curNode.nodeName === tagName) { return curNode; } // found + curNode = curNode.parentNode; // not found, so keep going up + } + + return null; // not found + }; + + /** + * Add all delegated event listeners + * + * All the events we care about bubble up to document, + * so we can take advantage of event delegation. + * + * Note: no need to wait for DOMContentLoaded here + */ + document.addEventListener('touchstart', touchstart, false); + document.addEventListener('touchmove', touchmove, false); + document.addEventListener('touchend', touchend, false); + document.addEventListener('click', click, true); // TODO: why does this use capture? + +})(); \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js new file mode 100644 index 000000000..d66f65893 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_imagesloaded.min.js @@ -0,0 +1,7 @@ +/*! + * imagesLoaded PACKAGED v3.1.8 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ + +(function(){function e(){}function t(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function n(e){return function(){return this[e].apply(this,arguments)}}var i=e.prototype,r=this,o=r.EventEmitter;i.getListeners=function(e){var t,n,i=this._getEvents();if("object"==typeof e){t={};for(n in i)i.hasOwnProperty(n)&&e.test(n)&&(t[n]=i[n])}else t=i[e]||(i[e]=[]);return t},i.flattenListeners=function(e){var t,n=[];for(t=0;e.length>t;t+=1)n.push(e[t].listener);return n},i.getListenersAsObject=function(e){var t,n=this.getListeners(e);return n instanceof Array&&(t={},t[e]=n),t||n},i.addListener=function(e,n){var i,r=this.getListenersAsObject(e),o="object"==typeof n;for(i in r)r.hasOwnProperty(i)&&-1===t(r[i],n)&&r[i].push(o?n:{listener:n,once:!1});return this},i.on=n("addListener"),i.addOnceListener=function(e,t){return this.addListener(e,{listener:t,once:!0})},i.once=n("addOnceListener"),i.defineEvent=function(e){return this.getListeners(e),this},i.defineEvents=function(e){for(var t=0;e.length>t;t+=1)this.defineEvent(e[t]);return this},i.removeListener=function(e,n){var i,r,o=this.getListenersAsObject(e);for(r in o)o.hasOwnProperty(r)&&(i=t(o[r],n),-1!==i&&o[r].splice(i,1));return this},i.off=n("removeListener"),i.addListeners=function(e,t){return this.manipulateListeners(!1,e,t)},i.removeListeners=function(e,t){return this.manipulateListeners(!0,e,t)},i.manipulateListeners=function(e,t,n){var i,r,o=e?this.removeListener:this.addListener,s=e?this.removeListeners:this.addListeners;if("object"!=typeof t||t instanceof RegExp)for(i=n.length;i--;)o.call(this,t,n[i]);else for(i in t)t.hasOwnProperty(i)&&(r=t[i])&&("function"==typeof r?o.call(this,i,r):s.call(this,i,r));return this},i.removeEvent=function(e){var t,n=typeof e,i=this._getEvents();if("string"===n)delete i[e];else if("object"===n)for(t in i)i.hasOwnProperty(t)&&e.test(t)&&delete i[t];else delete this._events;return this},i.removeAllListeners=n("removeEvent"),i.emitEvent=function(e,t){var n,i,r,o,s=this.getListenersAsObject(e);for(r in s)if(s.hasOwnProperty(r))for(i=s[r].length;i--;)n=s[r][i],n.once===!0&&this.removeListener(e,n.listener),o=n.listener.apply(this,t||[]),o===this._getOnceReturnValue()&&this.removeListener(e,n.listener);return this},i.trigger=n("emitEvent"),i.emit=function(e){var t=Array.prototype.slice.call(arguments,1);return this.emitEvent(e,t)},i.setOnceReturnValue=function(e){return this._onceReturnValue=e,this},i._getOnceReturnValue=function(){return this.hasOwnProperty("_onceReturnValue")?this._onceReturnValue:!0},i._getEvents=function(){return this._events||(this._events={})},e.noConflict=function(){return r.EventEmitter=o,e},"function"==typeof define&&define.amd?define("eventEmitter/EventEmitter",[],function(){return e}):"object"==typeof module&&module.exports?module.exports=e:this.EventEmitter=e}).call(this),function(e){function t(t){var n=e.event;return n.target=n.target||n.srcElement||t,n}var n=document.documentElement,i=function(){};n.addEventListener?i=function(e,t,n){e.addEventListener(t,n,!1)}:n.attachEvent&&(i=function(e,n,i){e[n+i]=i.handleEvent?function(){var n=t(e);i.handleEvent.call(i,n)}:function(){var n=t(e);i.call(e,n)},e.attachEvent("on"+n,e[n+i])});var r=function(){};n.removeEventListener?r=function(e,t,n){e.removeEventListener(t,n,!1)}:n.detachEvent&&(r=function(e,t,n){e.detachEvent("on"+t,e[t+n]);try{delete e[t+n]}catch(i){e[t+n]=void 0}});var o={bind:i,unbind:r};"function"==typeof define&&define.amd?define("eventie/eventie",o):e.eventie=o}(this),function(e,t){"function"==typeof define&&define.amd?define(["eventEmitter/EventEmitter","eventie/eventie"],function(n,i){return t(e,n,i)}):"object"==typeof exports?module.exports=t(e,require("wolfy87-eventemitter"),require("eventie")):e.imagesLoaded=t(e,e.EventEmitter,e.eventie)}(window,function(e,t,n){function i(e,t){for(var n in t)e[n]=t[n];return e}function r(e){return"[object Array]"===d.call(e)}function o(e){var t=[];if(r(e))t=e;else if("number"==typeof e.length)for(var n=0,i=e.length;i>n;n++)t.push(e[n]);else t.push(e);return t}function s(e,t,n){if(!(this instanceof s))return new s(e,t);"string"==typeof e&&(e=document.querySelectorAll(e)),this.elements=o(e),this.options=i({},this.options),"function"==typeof t?n=t:i(this.options,t),n&&this.on("always",n),this.getImages(),a&&(this.jqDeferred=new a.Deferred);var r=this;setTimeout(function(){r.check()})}function f(e){this.img=e}function c(e){this.src=e,v[e]=this}var a=e.jQuery,u=e.console,h=u!==void 0,d=Object.prototype.toString;s.prototype=new t,s.prototype.options={},s.prototype.getImages=function(){this.images=[];for(var e=0,t=this.elements.length;t>e;e++){var n=this.elements[e];"IMG"===n.nodeName&&this.addImage(n);var i=n.nodeType;if(i&&(1===i||9===i||11===i))for(var r=n.querySelectorAll("img"),o=0,s=r.length;s>o;o++){var f=r[o];this.addImage(f)}}},s.prototype.addImage=function(e){var t=new f(e);this.images.push(t)},s.prototype.check=function(){function e(e,r){return t.options.debug&&h&&u.log("confirm",e,r),t.progress(e),n++,n===i&&t.complete(),!0}var t=this,n=0,i=this.images.length;if(this.hasAnyBroken=!1,!i)return this.complete(),void 0;for(var r=0;i>r;r++){var o=this.images[r];o.on("confirm",e),o.check()}},s.prototype.progress=function(e){this.hasAnyBroken=this.hasAnyBroken||!e.isLoaded;var t=this;setTimeout(function(){t.emit("progress",t,e),t.jqDeferred&&t.jqDeferred.notify&&t.jqDeferred.notify(t,e)})},s.prototype.complete=function(){var e=this.hasAnyBroken?"fail":"done";this.isComplete=!0;var t=this;setTimeout(function(){if(t.emit(e,t),t.emit("always",t),t.jqDeferred){var n=t.hasAnyBroken?"reject":"resolve";t.jqDeferred[n](t)}})},a&&(a.fn.imagesLoaded=function(e,t){var n=new s(this,e,t);return n.jqDeferred.promise(a(this))}),f.prototype=new t,f.prototype.check=function(){var e=v[this.img.src]||new c(this.img.src);if(e.isConfirmed)return this.confirm(e.isLoaded,"cached was confirmed"),void 0;if(this.img.complete&&void 0!==this.img.naturalWidth)return this.confirm(0!==this.img.naturalWidth,"naturalWidth"),void 0;var t=this;e.on("confirm",function(e,n){return t.confirm(e.isLoaded,n),!0}),e.check()},f.prototype.confirm=function(e,t){this.isLoaded=e,this.emit("confirm",this,t)};var v={};return c.prototype=new t,c.prototype.check=function(){if(!this.isChecked){var e=new Image;n.bind(e,"load",this),n.bind(e,"error",this),e.src=this.src,this.isChecked=!0}},c.prototype.handleEvent=function(e){var t="on"+e.type;this[t]&&this[t](e)},c.prototype.onload=function(e){this.confirm(!0,"onload"),this.unbindProxyEvents(e)},c.prototype.onerror=function(e){this.confirm(!1,"onerror"),this.unbindProxyEvents(e)},c.prototype.confirm=function(e,t){this.isConfirmed=!0,this.isLoaded=e,this.emit("confirm",this,t)},c.prototype.unbindProxyEvents=function(e){n.unbind(e.target,"load",this),n.unbind(e.target,"error",this)},s}); \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js new file mode 100644 index 000000000..9dcf3c7af --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.highlight.js @@ -0,0 +1,108 @@ +/* + * jQuery Highlight plugin + * + * Based on highlight v3 by Johann Burkard + * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html + * + * Code a little bit refactored and cleaned (in my humble opinion). + * Most important changes: + * - has an option to highlight only entire words (wordsOnly - false by default), + * - has an option to be case sensitive (caseSensitive - false by default) + * - highlight element tag and class names can be specified in options + * + * Usage: + * // wrap every occurrance of text 'lorem' in content + * // with (default options) + * $('#content').highlight('lorem'); + * + * // search for and highlight more terms at once + * // so you can save some time on traversing DOM + * $('#content').highlight(['lorem', 'ipsum']); + * $('#content').highlight('lorem ipsum'); + * + * // search only for entire word 'lorem' + * $('#content').highlight('lorem', { wordsOnly: true }); + * + * // don't ignore case during search of term 'lorem' + * $('#content').highlight('lorem', { caseSensitive: true }); + * + * // wrap every occurrance of term 'ipsum' in content + * // with + * $('#content').highlight('ipsum', { element: 'em', className: 'important' }); + * + * // remove default highlight + * $('#content').unhighlight(); + * + * // remove custom highlight + * $('#content').unhighlight({ element: 'em', className: 'important' }); + * + * + * Copyright (c) 2009 Bartek Szopka + * + * Licensed under MIT license. + * + */ + +jQuery.extend({ + highlight: function (node, re, nodeName, className) { + if (node.nodeType === 3) { + var match = node.data.match(re); + if (match) { + var highlight = document.createElement(nodeName || 'span'); + highlight.className = className || 'highlight'; + var wordNode = node.splitText(match.index); + wordNode.splitText(match[0].length); + var wordClone = wordNode.cloneNode(true); + highlight.appendChild(wordClone); + wordNode.parentNode.replaceChild(highlight, wordNode); + return 1; //skip added node in parent + } + } else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children + !/(script|style)/i.test(node.tagName) && // ignore script and style nodes + !(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted + for (var i = 0; i < node.childNodes.length; i++) { + i += jQuery.highlight(node.childNodes[i], re, nodeName, className); + } + } + return 0; + } +}); + +jQuery.fn.unhighlight = function (options) { + var settings = { className: 'highlight', element: 'span' }; + jQuery.extend(settings, options); + + return this.find(settings.element + "." + settings.className).each(function () { + var parent = this.parentNode; + parent.replaceChild(this.firstChild, this); + parent.normalize(); + }).end(); +}; + +jQuery.fn.highlight = function (words, options) { + var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false }; + jQuery.extend(settings, options); + + if (words.constructor === String) { + words = [words]; + } + words = jQuery.grep(words, function(word, i){ + return word != ''; + }); + words = jQuery.map(words, function(word, i) { + return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + }); + if (words.length == 0) { return this; }; + + var flag = settings.caseSensitive ? "" : "i"; + var pattern = "(" + words.join("|") + ")"; + if (settings.wordsOnly) { + pattern = "\\b" + pattern + "\\b"; + } + var re = new RegExp(pattern, flag); + + return this.each(function () { + jQuery.highlight(this, re, settings.element, settings.className); + }); +}; + diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js new file mode 100644 index 000000000..91cf63791 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.tocify.js @@ -0,0 +1,1042 @@ +/* jquery Tocify - v1.8.0 - 2013-09-16 +* http://www.gregfranko.com/jquery.tocify.js/ +* Copyright (c) 2013 Greg Franko; Licensed MIT +* Modified lightly by Robert Lord to fix a bug I found, +* and also so it adds ids to headers +* also because I want height caching, since the +* height lookup for h1s and h2s was causing serious +* lag spikes below 30 fps */ + +// Immediately-Invoked Function Expression (IIFE) [Ben Alman Blog Post](http://benalman.com/news/2010/11/immediately-invoked-function-expression/) that calls another IIFE that contains all of the plugin logic. I used this pattern so that anyone viewing this code would not have to scroll to the bottom of the page to view the local parameters that were passed to the main IIFE. +(function(tocify) { + + // ECMAScript 5 Strict Mode: [John Resig Blog Post](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/) + "use strict"; + + // Calls the second IIFE and locally passes in the global jQuery, window, and document objects + tocify(window.jQuery, window, document); + +} + +// Locally passes in `jQuery`, the `window` object, the `document` object, and an `undefined` variable. The `jQuery`, `window` and `document` objects are passed in locally, to improve performance, since javascript first searches for a variable match within the local variables set before searching the global variables set. All of the global variables are also passed in locally to be minifier friendly. `undefined` can be passed in locally, because it is not a reserved word in JavaScript. +(function($, window, document, undefined) { + + // ECMAScript 5 Strict Mode: [John Resig Blog Post](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/) + "use strict"; + + var tocClassName = "tocify", + tocClass = "." + tocClassName, + tocFocusClassName = "tocify-focus", + tocHoverClassName = "tocify-hover", + hideTocClassName = "tocify-hide", + hideTocClass = "." + hideTocClassName, + headerClassName = "tocify-header", + headerClass = "." + headerClassName, + subheaderClassName = "tocify-subheader", + subheaderClass = "." + subheaderClassName, + itemClassName = "tocify-item", + itemClass = "." + itemClassName, + extendPageClassName = "tocify-extend-page", + extendPageClass = "." + extendPageClassName; + + // Calling the jQueryUI Widget Factory Method + $.widget("toc.tocify", { + + //Plugin version + version: "1.8.0", + + // These options will be used as defaults + options: { + + // **context**: Accepts String: Any jQuery selector + // The container element that holds all of the elements used to generate the table of contents + context: "body", + + // **ignoreSelector**: Accepts String: Any jQuery selector + // A selector to any element that would be matched by selectors that you wish to be ignored + ignoreSelector: null, + + // **selectors**: Accepts an Array of Strings: Any jQuery selectors + // The element's used to generate the table of contents. The order is very important since it will determine the table of content's nesting structure + selectors: "h1, h2, h3", + + // **showAndHide**: Accepts a boolean: true or false + // Used to determine if elements should be shown and hidden + showAndHide: true, + + // **showEffect**: Accepts String: "none", "fadeIn", "show", or "slideDown" + // Used to display any of the table of contents nested items + showEffect: "slideDown", + + // **showEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" + // The time duration of the show animation + showEffectSpeed: "medium", + + // **hideEffect**: Accepts String: "none", "fadeOut", "hide", or "slideUp" + // Used to hide any of the table of contents nested items + hideEffect: "slideUp", + + // **hideEffectSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" + // The time duration of the hide animation + hideEffectSpeed: "medium", + + // **smoothScroll**: Accepts a boolean: true or false + // Determines if a jQuery animation should be used to scroll to specific table of contents items on the page + smoothScroll: true, + + // **smoothScrollSpeed**: Accepts Number (milliseconds) or String: "slow", "medium", or "fast" + // The time duration of the smoothScroll animation + smoothScrollSpeed: "medium", + + // **scrollTo**: Accepts Number (pixels) + // The amount of space between the top of page and the selected table of contents item after the page has been scrolled + scrollTo: 0, + + // **showAndHideOnScroll**: Accepts a boolean: true or false + // Determines if table of contents nested items should be shown and hidden while scrolling + showAndHideOnScroll: true, + + // **highlightOnScroll**: Accepts a boolean: true or false + // Determines if table of contents nested items should be highlighted (set to a different color) while scrolling + highlightOnScroll: true, + + // **highlightOffset**: Accepts a number + // The offset distance in pixels to trigger the next active table of contents item + highlightOffset: 40, + + // **theme**: Accepts a string: "bootstrap", "jqueryui", or "none" + // Determines if Twitter Bootstrap, jQueryUI, or Tocify classes should be added to the table of contents + theme: "bootstrap", + + // **extendPage**: Accepts a boolean: true or false + // If a user scrolls to the bottom of the page and the page is not tall enough to scroll to the last table of contents item, then the page height is increased + extendPage: true, + + // **extendPageOffset**: Accepts a number: pixels + // How close to the bottom of the page a user must scroll before the page is extended + extendPageOffset: 100, + + // **history**: Accepts a boolean: true or false + // Adds a hash to the page url to maintain history + history: true, + + // **scrollHistory**: Accepts a boolean: true or false + // Adds a hash to the page url, to maintain history, when scrolling to a TOC item + scrollHistory: false, + + // **hashGenerator**: How the hash value (the anchor segment of the URL, following the + // # character) will be generated. + // + // "compact" (default) - #CompressesEverythingTogether + // "pretty" - #looks-like-a-nice-url-and-is-easily-readable + // function(text, element){} - Your own hash generation function that accepts the text as an + // argument, and returns the hash value. + hashGenerator: "compact", + + // **highlightDefault**: Accepts a boolean: true or false + // Set's the first TOC item as active if no other TOC item is active. + highlightDefault: true + + }, + + // _Create + // ------- + // Constructs the plugin. Only called once. + _create: function() { + + var self = this; + + self.tocifyWrapper = $('.tocify-wrapper'); + self.extendPageScroll = true; + + // Internal array that keeps track of all TOC items (Helps to recognize if there are duplicate TOC item strings) + self.items = []; + + // Generates the HTML for the dynamic table of contents + self._generateToc(); + + // Caches heights and anchors + self.cachedHeights = [], + self.cachedAnchors = []; + + // Adds CSS classes to the newly generated table of contents HTML + self._addCSSClasses(); + + self.webkit = (function() { + + for(var prop in window) { + + if(prop) { + + if(prop.toLowerCase().indexOf("webkit") !== -1) { + + return true; + + } + + } + + } + + return false; + + }()); + + // Adds jQuery event handlers to the newly generated table of contents + self._setEventHandlers(); + + // Binding to the Window load event to make sure the correct scrollTop is calculated + $(window).load(function() { + + // Sets the active TOC item + self._setActiveElement(true); + + // Once all animations on the page are complete, this callback function will be called + $("html, body").promise().done(function() { + + setTimeout(function() { + + self.extendPageScroll = false; + + },0); + + }); + + }); + + }, + + // _generateToc + // ------------ + // Generates the HTML for the dynamic table of contents + _generateToc: function() { + + // _Local variables_ + + // Stores the plugin context in the self variable + var self = this, + + // All of the HTML tags found within the context provided (i.e. body) that match the top level jQuery selector above + firstElem, + + // Instantiated variable that will store the top level newly created unordered list DOM element + ul, + ignoreSelector = self.options.ignoreSelector; + + // If the selectors option has a comma within the string + if(this.options.selectors.indexOf(",") !== -1) { + + // Grabs the first selector from the string + firstElem = $(this.options.context).find(this.options.selectors.replace(/ /g,"").substr(0, this.options.selectors.indexOf(","))); + + } + + // If the selectors option does not have a comman within the string + else { + + // Grabs the first selector from the string and makes sure there are no spaces + firstElem = $(this.options.context).find(this.options.selectors.replace(/ /g,"")); + + } + + if(!firstElem.length) { + + self.element.addClass(hideTocClassName); + + return; + + } + + self.element.addClass(tocClassName); + + // Loops through each top level selector + firstElem.each(function(index) { + + //If the element matches the ignoreSelector then we skip it + if($(this).is(ignoreSelector)) { + return; + } + + // Creates an unordered list HTML element and adds a dynamic ID and standard class name + ul = $("
      ", { + "id": headerClassName + index, + "class": headerClassName + }). + + // Appends a top level list item HTML element to the previously created HTML header + append(self._nestElements($(this), index)); + + // Add the created unordered list element to the HTML element calling the plugin + self.element.append(ul); + + // Finds all of the HTML tags between the header and subheader elements + $(this).nextUntil(this.nodeName.toLowerCase()).each(function() { + + // If there are no nested subheader elemements + if($(this).find(self.options.selectors).length === 0) { + + // Loops through all of the subheader elements + $(this).filter(self.options.selectors).each(function() { + + //If the element matches the ignoreSelector then we skip it + if($(this).is(ignoreSelector)) { + return; + } + + self._appendSubheaders.call(this, self, ul); + + }); + + } + + // If there are nested subheader elements + else { + + // Loops through all of the subheader elements + $(this).find(self.options.selectors).each(function() { + + //If the element matches the ignoreSelector then we skip it + if($(this).is(ignoreSelector)) { + return; + } + + self._appendSubheaders.call(this, self, ul); + + }); + + } + + }); + + }); + + }, + + _setActiveElement: function(pageload) { + + var self = this, + + hash = window.location.hash.substring(1), + + elem = self.element.find("li[data-unique='" + hash + "']"); + + if(hash.length) { + + // Removes highlighting from all of the list item's + self.element.find("." + self.focusClass).removeClass(self.focusClass); + + // Highlights the current list item that was clicked + elem.addClass(self.focusClass); + + // If the showAndHide option is true + if(self.options.showAndHide) { + + // Triggers the click event on the currently focused TOC item + elem.click(); + + } + + } + + else { + + // Removes highlighting from all of the list item's + self.element.find("." + self.focusClass).removeClass(self.focusClass); + + if(!hash.length && pageload && self.options.highlightDefault) { + + // Highlights the first TOC item if no other items are highlighted + self.element.find(itemClass).first().addClass(self.focusClass); + + } + + } + + return self; + + }, + + // _nestElements + // ------------- + // Helps create the table of contents list by appending nested list items + _nestElements: function(self, index) { + + var arr, item, hashValue; + + arr = $.grep(this.items, function (item) { + + return item === self.text(); + + }); + + // If there is already a duplicate TOC item + if(arr.length) { + + // Adds the current TOC item text and index (for slight randomization) to the internal array + this.items.push(self.text() + index); + + } + + // If there not a duplicate TOC item + else { + + // Adds the current TOC item text to the internal array + this.items.push(self.text()); + + } + + hashValue = this._generateHashValue(arr, self, index); + + // ADDED BY ROBERT + // actually add the hash value to the element's id + // self.attr("id", "link-" + hashValue); + + // Appends a list item HTML element to the last unordered list HTML element found within the HTML element calling the plugin + item = $("
    • ", { + + // Sets a common class name to the list item + "class": itemClassName, + + "data-unique": hashValue + + }).append($("", { + + "text": self.text() + + })); + + // Adds an HTML anchor tag before the currently traversed HTML element + self.before($("
      ", { + + // Sets a name attribute on the anchor tag to the text of the currently traversed HTML element (also making sure that all whitespace is replaced with an underscore) + "name": hashValue, + + "data-unique": hashValue + + })); + + return item; + + }, + + // _generateHashValue + // ------------------ + // Generates the hash value that will be used to refer to each item. + _generateHashValue: function(arr, self, index) { + + var hashValue = "", + hashGeneratorOption = this.options.hashGenerator; + + if (hashGeneratorOption === "pretty") { + // remove weird characters + + + // prettify the text + hashValue = self.text().toLowerCase().replace(/\s/g, "-"); + + // ADDED BY ROBERT + // remove weird characters + hashValue = hashValue.replace(/[^\x00-\x7F]/g, ""); + + // fix double hyphens + while (hashValue.indexOf("--") > -1) { + hashValue = hashValue.replace(/--/g, "-"); + } + + // fix colon-space instances + while (hashValue.indexOf(":-") > -1) { + hashValue = hashValue.replace(/:-/g, "-"); + } + + } else if (typeof hashGeneratorOption === "function") { + + // call the function + hashValue = hashGeneratorOption(self.text(), self); + + } else { + + // compact - the default + hashValue = self.text().replace(/\s/g, ""); + + } + + // add the index if we need to + if (arr.length) { hashValue += ""+index; } + + // return the value + return hashValue; + + }, + + // _appendElements + // --------------- + // Helps create the table of contents list by appending subheader elements + + _appendSubheaders: function(self, ul) { + + // The current element index + var index = $(this).index(self.options.selectors), + + // Finds the previous header DOM element + previousHeader = $(self.options.selectors).eq(index - 1), + + currentTagName = +$(this).prop("tagName").charAt(1), + + previousTagName = +previousHeader.prop("tagName").charAt(1), + + lastSubheader; + + // If the current header DOM element is smaller than the previous header DOM element or the first subheader + if(currentTagName < previousTagName) { + + // Selects the last unordered list HTML found within the HTML element calling the plugin + self.element.find(subheaderClass + "[data-tag=" + currentTagName + "]").last().append(self._nestElements($(this), index)); + + } + + // If the current header DOM element is the same type of header(eg. h4) as the previous header DOM element + else if(currentTagName === previousTagName) { + + ul.find(itemClass).last().after(self._nestElements($(this), index)); + + } + + else { + + // Selects the last unordered list HTML found within the HTML element calling the plugin + ul.find(itemClass).last(). + + // Appends an unorderedList HTML element to the dynamic `unorderedList` variable and sets a common class name + after($("
        ", { + + "class": subheaderClassName, + + "data-tag": currentTagName + + })).next(subheaderClass). + + // Appends a list item HTML element to the last unordered list HTML element found within the HTML element calling the plugin + append(self._nestElements($(this), index)); + } + + }, + + // _setEventHandlers + // ---------------- + // Adds jQuery event handlers to the newly generated table of contents + _setEventHandlers: function() { + + // _Local variables_ + + // Stores the plugin context in the self variable + var self = this, + + // Instantiates a new variable that will be used to hold a specific element's context + $self, + + // Instantiates a new variable that will be used to determine the smoothScroll animation time duration + duration; + + // Event delegation that looks for any clicks on list item elements inside of the HTML element calling the plugin + this.element.on("click.tocify", "li", function(event) { + + if(self.options.history) { + + window.location.hash = $(this).attr("data-unique"); + + } + + // Removes highlighting from all of the list item's + self.element.find("." + self.focusClass).removeClass(self.focusClass); + + // Highlights the current list item that was clicked + $(this).addClass(self.focusClass); + + // If the showAndHide option is true + if(self.options.showAndHide) { + + var elem = $('li[data-unique="' + $(this).attr("data-unique") + '"]'); + + self._triggerShow(elem); + + } + + self._scrollTo($(this)); + + }); + + // Mouseenter and Mouseleave event handlers for the list item's within the HTML element calling the plugin + this.element.find("li").on({ + + // Mouseenter event handler + "mouseenter.tocify": function() { + + // Adds a hover CSS class to the current list item + $(this).addClass(self.hoverClass); + + // Makes sure the cursor is set to the pointer icon + $(this).css("cursor", "pointer"); + + }, + + // Mouseleave event handler + "mouseleave.tocify": function() { + + if(self.options.theme !== "bootstrap") { + + // Removes the hover CSS class from the current list item + $(this).removeClass(self.hoverClass); + + } + + } + }); + + // Reset height cache on scroll + + $(window).on('resize', function() { + self.calculateHeights(); + }); + + // Window scroll event handler + $(window).on("scroll.tocify", function() { + + // Once all animations on the page are complete, this callback function will be called + $("html, body").promise().done(function() { + + // Local variables + + // Stores how far the user has scrolled + var winScrollTop = $(window).scrollTop(), + + // Stores the height of the window + winHeight = $(window).height(), + + // Stores the height of the document + docHeight = $(document).height(), + + scrollHeight = $("body")[0].scrollHeight, + + // Instantiates a variable that will be used to hold a selected HTML element + elem, + + lastElem, + + lastElemOffset, + + currentElem; + + if(self.options.extendPage) { + + // If the user has scrolled to the bottom of the page and the last toc item is not focused + if((self.webkit && winScrollTop >= scrollHeight - winHeight - self.options.extendPageOffset) || (!self.webkit && winHeight + winScrollTop > docHeight - self.options.extendPageOffset)) { + + if(!$(extendPageClass).length) { + + lastElem = $('div[data-unique="' + $(itemClass).last().attr("data-unique") + '"]'); + + if(!lastElem.length) return; + + // Gets the top offset of the page header that is linked to the last toc item + lastElemOffset = lastElem.offset().top; + + // Appends a div to the bottom of the page and sets the height to the difference of the window scrollTop and the last element's position top offset + $(self.options.context).append($("
        ", { + + "class": extendPageClassName, + + "height": Math.abs(lastElemOffset - winScrollTop) + "px", + + "data-unique": extendPageClassName + + })); + + if(self.extendPageScroll) { + + currentElem = self.element.find('li.active'); + + self._scrollTo($("div[data-unique=" + currentElem.attr("data-unique") + "]")); + + } + + } + + } + + } + + // The zero timeout ensures the following code is run after the scroll events + setTimeout(function() { + + // _Local variables_ + + // Stores the distance to the closest anchor + var // Stores the index of the closest anchor + closestAnchorIdx = null, + anchorText; + + // if never calculated before, calculate and cache the heights + if (self.cachedHeights.length == 0) { + self.calculateHeights(); + } + + var scrollTop = $(window).scrollTop(); + + // Determines the index of the closest anchor + self.cachedAnchors.each(function(idx) { + if (self.cachedHeights[idx] - scrollTop < 0) { + closestAnchorIdx = idx; + } else { + return false; + } + }); + + anchorText = $(self.cachedAnchors[closestAnchorIdx]).attr("data-unique"); + + // Stores the list item HTML element that corresponds to the currently traversed anchor tag + elem = $('li[data-unique="' + anchorText + '"]'); + + // If the `highlightOnScroll` option is true and a next element is found + if(self.options.highlightOnScroll && elem.length && !elem.hasClass(self.focusClass)) { + + // Removes highlighting from all of the list item's + self.element.find("." + self.focusClass).removeClass(self.focusClass); + + // Highlights the corresponding list item + elem.addClass(self.focusClass); + + // Scroll to highlighted element's header + var tocifyWrapper = self.tocifyWrapper; + var scrollToElem = $(elem).closest('.tocify-header'); + + var elementOffset = scrollToElem.offset().top, + wrapperOffset = tocifyWrapper.offset().top; + var offset = elementOffset - wrapperOffset; + + if (offset >= $(window).height()) { + var scrollPosition = offset + tocifyWrapper.scrollTop(); + tocifyWrapper.scrollTop(scrollPosition); + } else if (offset < 0) { + tocifyWrapper.scrollTop(0); + } + } + + if(self.options.scrollHistory) { + + // IF STATEMENT ADDED BY ROBERT + + if(window.location.hash !== "#" + anchorText && anchorText !== undefined) { + + if(history.replaceState) { + history.replaceState({}, "", "#" + anchorText); + // provide a fallback + } else { + scrollV = document.body.scrollTop; + scrollH = document.body.scrollLeft; + location.hash = "#" + anchorText; + document.body.scrollTop = scrollV; + document.body.scrollLeft = scrollH; + } + + } + + } + + // If the `showAndHideOnScroll` option is true + if(self.options.showAndHideOnScroll && self.options.showAndHide) { + + self._triggerShow(elem, true); + + } + + }, 0); + + }); + + }); + + }, + + // calculateHeights + // ---- + // ADDED BY ROBERT + calculateHeights: function() { + var self = this; + self.cachedHeights = []; + self.cachedAnchors = []; + var anchors = $(self.options.context).find("div[data-unique]"); + anchors.each(function(idx) { + var distance = (($(this).next().length ? $(this).next() : $(this)).offset().top - self.options.highlightOffset); + self.cachedHeights[idx] = distance; + }); + self.cachedAnchors = anchors; + }, + + // Show + // ---- + // Opens the current sub-header + show: function(elem, scroll) { + + // Stores the plugin context in the `self` variable + var self = this, + element = elem; + + // If the sub-header is not already visible + if (!elem.is(":visible")) { + + // If the current element does not have any nested subheaders, is not a header, and its parent is not visible + if(!elem.find(subheaderClass).length && !elem.parent().is(headerClass) && !elem.parent().is(":visible")) { + + // Sets the current element to all of the subheaders within the current header + elem = elem.parents(subheaderClass).add(elem); + + } + + // If the current element does not have any nested subheaders and is not a header + else if(!elem.children(subheaderClass).length && !elem.parent().is(headerClass)) { + + // Sets the current element to the closest subheader + elem = elem.closest(subheaderClass); + + } + + //Determines what jQuery effect to use + switch (self.options.showEffect) { + + //Uses `no effect` + case "none": + + elem.show(); + + break; + + //Uses the jQuery `show` special effect + case "show": + + elem.show(self.options.showEffectSpeed); + + break; + + //Uses the jQuery `slideDown` special effect + case "slideDown": + + elem.slideDown(self.options.showEffectSpeed); + + break; + + //Uses the jQuery `fadeIn` special effect + case "fadeIn": + + elem.fadeIn(self.options.showEffectSpeed); + + break; + + //If none of the above options were passed, then a `jQueryUI show effect` is expected + default: + + elem.show(); + + break; + + } + + } + + // If the current subheader parent element is a header + if(elem.parent().is(headerClass)) { + + // Hides all non-active sub-headers + self.hide($(subheaderClass).not(elem)); + + } + + // If the current subheader parent element is not a header + else { + + // Hides all non-active sub-headers + self.hide($(subheaderClass).not(elem.closest(headerClass).find(subheaderClass).not(elem.siblings()))); + + } + + // Maintains chainablity + return self; + + }, + + // Hide + // ---- + // Closes the current sub-header + hide: function(elem) { + + // Stores the plugin context in the `self` variable + var self = this; + + //Determines what jQuery effect to use + switch (self.options.hideEffect) { + + // Uses `no effect` + case "none": + + elem.hide(); + + break; + + // Uses the jQuery `hide` special effect + case "hide": + + elem.hide(self.options.hideEffectSpeed); + + break; + + // Uses the jQuery `slideUp` special effect + case "slideUp": + + elem.slideUp(self.options.hideEffectSpeed); + + break; + + // Uses the jQuery `fadeOut` special effect + case "fadeOut": + + elem.fadeOut(self.options.hideEffectSpeed); + + break; + + // If none of the above options were passed, then a `jqueryUI hide effect` is expected + default: + + elem.hide(); + + break; + + } + + // Maintains chainablity + return self; + }, + + // _triggerShow + // ------------ + // Determines what elements get shown on scroll and click + _triggerShow: function(elem, scroll) { + + var self = this; + + // If the current element's parent is a header element or the next element is a nested subheader element + if(elem.parent().is(headerClass) || elem.next().is(subheaderClass)) { + + // Shows the next sub-header element + self.show(elem.next(subheaderClass), scroll); + + } + + // If the current element's parent is a subheader element + else if(elem.parent().is(subheaderClass)) { + + // Shows the parent sub-header element + self.show(elem.parent(), scroll); + + } + + // Maintains chainability + return self; + + }, + + // _addCSSClasses + // -------------- + // Adds CSS classes to the newly generated table of contents HTML + _addCSSClasses: function() { + + // If the user wants a jqueryUI theme + if(this.options.theme === "jqueryui") { + + this.focusClass = "ui-state-default"; + + this.hoverClass = "ui-state-hover"; + + //Adds the default styling to the dropdown list + this.element.addClass("ui-widget").find(".toc-title").addClass("ui-widget-header").end().find("li").addClass("ui-widget-content"); + + } + + // If the user wants a twitterBootstrap theme + else if(this.options.theme === "bootstrap") { + + this.element.find(headerClass + "," + subheaderClass).addClass("nav nav-list"); + + this.focusClass = "active"; + + } + + // If a user does not want a prebuilt theme + else { + + // Adds more neutral classes (instead of jqueryui) + + this.focusClass = tocFocusClassName; + + this.hoverClass = tocHoverClassName; + + } + + //Maintains chainability + return this; + + }, + + // setOption + // --------- + // Sets a single Tocify option after the plugin is invoked + setOption: function() { + + // Calls the jQueryUI Widget Factory setOption method + $.Widget.prototype._setOption.apply(this, arguments); + + }, + + // setOptions + // ---------- + // Sets a single or multiple Tocify options after the plugin is invoked + setOptions: function() { + + // Calls the jQueryUI Widget Factory setOptions method + $.Widget.prototype._setOptions.apply(this, arguments); + + }, + + // _scrollTo + // --------- + // Scrolls to a specific element + _scrollTo: function(elem) { + + var self = this, + duration = self.options.smoothScroll || 0, + scrollTo = self.options.scrollTo; + + // Once all animations on the page are complete, this callback function will be called + $("html, body").promise().done(function() { + + // Animates the html and body element scrolltops + $("html, body").animate({ + + // Sets the jQuery `scrollTop` to the top offset of the HTML div tag that matches the current list item's `data-unique` tag + "scrollTop": $('div[data-unique="' + elem.attr("data-unique") + '"]').next().offset().top - ($.isFunction(scrollTo) ? scrollTo.call() : scrollTo) + "px" + + }, { + + // Sets the smoothScroll animation time duration to the smoothScrollSpeed option + "duration": duration + + }); + + }); + + // Maintains chainability + return self; + + } + + }); + +})); //end of plugin diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js new file mode 100644 index 000000000..637e9c142 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery_ui.js @@ -0,0 +1,566 @@ +/*! jQuery UI - v1.11.3 - 2015-02-12 + * http://jqueryui.com + * Includes: widget.js + * Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +(function( factory ) { + if ( typeof define === "function" && define.amd ) { + + // AMD. Register as an anonymous module. + define([ "jquery" ], factory ); + } else { + + // Browser globals + factory( jQuery ); + } +}(function( $ ) { + /*! + * jQuery UI Widget 1.11.3 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ + + + var widget_uuid = 0, + widget_slice = Array.prototype.slice; + + $.cleanData = (function( orig ) { + return function( elems ) { + var events, elem, i; + for ( i = 0; (elem = elems[i]) != null; i++ ) { + try { + + // Only trigger remove when necessary to save time + events = $._data( elem, "events" ); + if ( events && events.remove ) { + $( elem ).triggerHandler( "remove" ); + } + + // http://bugs.jquery.com/ticket/8235 + } catch ( e ) {} + } + orig( elems ); + }; + })( $.cleanData ); + + $.widget = function( name, base, prototype ) { + var fullName, existingConstructor, constructor, basePrototype, + // proxiedPrototype allows the provided prototype to remain unmodified + // so that it can be used as a mixin for multiple widgets (#8876) + proxiedPrototype = {}, + namespace = name.split( "." )[ 0 ]; + + name = name.split( "." )[ 1 ]; + fullName = namespace + "-" + name; + + if ( !prototype ) { + prototype = base; + base = $.Widget; + } + + // create selector for plugin + $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { + return !!$.data( elem, fullName ); + }; + + $[ namespace ] = $[ namespace ] || {}; + existingConstructor = $[ namespace ][ name ]; + constructor = $[ namespace ][ name ] = function( options, element ) { + // allow instantiation without "new" keyword + if ( !this._createWidget ) { + return new constructor( options, element ); + } + + // allow instantiation without initializing for simple inheritance + // must use "new" keyword (the code above always passes args) + if ( arguments.length ) { + this._createWidget( options, element ); + } + }; + // extend with the existing constructor to carry over any static properties + $.extend( constructor, existingConstructor, { + version: prototype.version, + // copy the object used to create the prototype in case we need to + // redefine the widget later + _proto: $.extend( {}, prototype ), + // track widgets that inherit from this widget in case this widget is + // redefined after a widget inherits from it + _childConstructors: [] + }); + + basePrototype = new base(); + // we need to make the options hash a property directly on the new instance + // otherwise we'll modify the options hash on the prototype that we're + // inheriting from + basePrototype.options = $.widget.extend( {}, basePrototype.options ); + $.each( prototype, function( prop, value ) { + if ( !$.isFunction( value ) ) { + proxiedPrototype[ prop ] = value; + return; + } + proxiedPrototype[ prop ] = (function() { + var _super = function() { + return base.prototype[ prop ].apply( this, arguments ); + }, + _superApply = function( args ) { + return base.prototype[ prop ].apply( this, args ); + }; + return function() { + var __super = this._super, + __superApply = this._superApply, + returnValue; + + this._super = _super; + this._superApply = _superApply; + + returnValue = value.apply( this, arguments ); + + this._super = __super; + this._superApply = __superApply; + + return returnValue; + }; + })(); + }); + constructor.prototype = $.widget.extend( basePrototype, { + // TODO: remove support for widgetEventPrefix + // always use the name + a colon as the prefix, e.g., draggable:start + // don't prefix for widgets that aren't DOM-based + widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name + }, proxiedPrototype, { + constructor: constructor, + namespace: namespace, + widgetName: name, + widgetFullName: fullName + }); + + // If this widget is being redefined then we need to find all widgets that + // are inheriting from it and redefine all of them so that they inherit from + // the new version of this widget. We're essentially trying to replace one + // level in the prototype chain. + if ( existingConstructor ) { + $.each( existingConstructor._childConstructors, function( i, child ) { + var childPrototype = child.prototype; + + // redefine the child widget using the same prototype that was + // originally used, but inherit from the new version of the base + $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); + }); + // remove the list of existing child constructors from the old constructor + // so the old child constructors can be garbage collected + delete existingConstructor._childConstructors; + } else { + base._childConstructors.push( constructor ); + } + + $.widget.bridge( name, constructor ); + + return constructor; + }; + + $.widget.extend = function( target ) { + var input = widget_slice.call( arguments, 1 ), + inputIndex = 0, + inputLength = input.length, + key, + value; + for ( ; inputIndex < inputLength; inputIndex++ ) { + for ( key in input[ inputIndex ] ) { + value = input[ inputIndex ][ key ]; + if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { + // Clone objects + if ( $.isPlainObject( value ) ) { + target[ key ] = $.isPlainObject( target[ key ] ) ? + $.widget.extend( {}, target[ key ], value ) : + // Don't extend strings, arrays, etc. with objects + $.widget.extend( {}, value ); + // Copy everything else by reference + } else { + target[ key ] = value; + } + } + } + } + return target; + }; + + $.widget.bridge = function( name, object ) { + var fullName = object.prototype.widgetFullName || name; + $.fn[ name ] = function( options ) { + var isMethodCall = typeof options === "string", + args = widget_slice.call( arguments, 1 ), + returnValue = this; + + if ( isMethodCall ) { + this.each(function() { + var methodValue, + instance = $.data( this, fullName ); + if ( options === "instance" ) { + returnValue = instance; + return false; + } + if ( !instance ) { + return $.error( "cannot call methods on " + name + " prior to initialization; " + + "attempted to call method '" + options + "'" ); + } + if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { + return $.error( "no such method '" + options + "' for " + name + " widget instance" ); + } + methodValue = instance[ options ].apply( instance, args ); + if ( methodValue !== instance && methodValue !== undefined ) { + returnValue = methodValue && methodValue.jquery ? + returnValue.pushStack( methodValue.get() ) : + methodValue; + return false; + } + }); + } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat(args) ); + } + + this.each(function() { + var instance = $.data( this, fullName ); + if ( instance ) { + instance.option( options || {} ); + if ( instance._init ) { + instance._init(); + } + } else { + $.data( this, fullName, new object( options, this ) ); + } + }); + } + + return returnValue; + }; + }; + + $.Widget = function( /* options, element */ ) {}; + $.Widget._childConstructors = []; + + $.Widget.prototype = { + widgetName: "widget", + widgetEventPrefix: "", + defaultElement: "
        ", + options: { + disabled: false, + + // callbacks + create: null + }, + _createWidget: function( options, element ) { + element = $( element || this.defaultElement || this )[ 0 ]; + this.element = $( element ); + this.uuid = widget_uuid++; + this.eventNamespace = "." + this.widgetName + this.uuid; + + this.bindings = $(); + this.hoverable = $(); + this.focusable = $(); + + if ( element !== this ) { + $.data( element, this.widgetFullName, this ); + this._on( true, this.element, { + remove: function( event ) { + if ( event.target === element ) { + this.destroy(); + } + } + }); + this.document = $( element.style ? + // element within the document + element.ownerDocument : + // element is window or document + element.document || element ); + this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); + } + + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + + this._create(); + this._trigger( "create", null, this._getCreateEventData() ); + this._init(); + }, + _getCreateOptions: $.noop, + _getCreateEventData: $.noop, + _create: $.noop, + _init: $.noop, + + destroy: function() { + this._destroy(); + // we can probably remove the unbind calls in 2.0 + // all event bindings should go through this._on() + this.element + .unbind( this.eventNamespace ) + .removeData( this.widgetFullName ) + // support: jquery <1.6.3 + // http://bugs.jquery.com/ticket/9413 + .removeData( $.camelCase( this.widgetFullName ) ); + this.widget() + .unbind( this.eventNamespace ) + .removeAttr( "aria-disabled" ) + .removeClass( + this.widgetFullName + "-disabled " + + "ui-state-disabled" ); + + // clean up events and states + this.bindings.unbind( this.eventNamespace ); + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + }, + _destroy: $.noop, + + widget: function() { + return this.element; + }, + + option: function( key, value ) { + var options = key, + parts, + curOption, + i; + + if ( arguments.length === 0 ) { + // don't return a reference to the internal hash + return $.widget.extend( {}, this.options ); + } + + if ( typeof key === "string" ) { + // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } + options = {}; + parts = key.split( "." ); + key = parts.shift(); + if ( parts.length ) { + curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); + for ( i = 0; i < parts.length - 1; i++ ) { + curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; + curOption = curOption[ parts[ i ] ]; + } + key = parts.pop(); + if ( arguments.length === 1 ) { + return curOption[ key ] === undefined ? null : curOption[ key ]; + } + curOption[ key ] = value; + } else { + if ( arguments.length === 1 ) { + return this.options[ key ] === undefined ? null : this.options[ key ]; + } + options[ key ] = value; + } + } + + this._setOptions( options ); + + return this; + }, + _setOptions: function( options ) { + var key; + + for ( key in options ) { + this._setOption( key, options[ key ] ); + } + + return this; + }, + _setOption: function( key, value ) { + this.options[ key ] = value; + + if ( key === "disabled" ) { + this.widget() + .toggleClass( this.widgetFullName + "-disabled", !!value ); + + // If the widget is becoming disabled, then nothing is interactive + if ( value ) { + this.hoverable.removeClass( "ui-state-hover" ); + this.focusable.removeClass( "ui-state-focus" ); + } + } + + return this; + }, + + enable: function() { + return this._setOptions({ disabled: false }); + }, + disable: function() { + return this._setOptions({ disabled: true }); + }, + + _on: function( suppressDisabledCheck, element, handlers ) { + var delegateElement, + instance = this; + + // no suppressDisabledCheck flag, shuffle arguments + if ( typeof suppressDisabledCheck !== "boolean" ) { + handlers = element; + element = suppressDisabledCheck; + suppressDisabledCheck = false; + } + + // no element argument, shuffle and use this.element + if ( !handlers ) { + handlers = element; + element = this.element; + delegateElement = this.widget(); + } else { + element = delegateElement = $( element ); + this.bindings = this.bindings.add( element ); + } + + $.each( handlers, function( event, handler ) { + function handlerProxy() { + // allow widgets to customize the disabled handling + // - disabled as an array instead of boolean + // - disabled class as method for disabling individual parts + if ( !suppressDisabledCheck && + ( instance.options.disabled === true || + $( this ).hasClass( "ui-state-disabled" ) ) ) { + return; + } + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + + // copy the guid so direct unbinding works + if ( typeof handler !== "string" ) { + handlerProxy.guid = handler.guid = + handler.guid || handlerProxy.guid || $.guid++; + } + + var match = event.match( /^([\w:-]*)\s*(.*)$/ ), + eventName = match[1] + instance.eventNamespace, + selector = match[2]; + if ( selector ) { + delegateElement.delegate( selector, eventName, handlerProxy ); + } else { + element.bind( eventName, handlerProxy ); + } + }); + }, + + _off: function( element, eventName ) { + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; + element.unbind( eventName ).undelegate( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); + }, + + _delay: function( handler, delay ) { + function handlerProxy() { + return ( typeof handler === "string" ? instance[ handler ] : handler ) + .apply( instance, arguments ); + } + var instance = this; + return setTimeout( handlerProxy, delay || 0 ); + }, + + _hoverable: function( element ) { + this.hoverable = this.hoverable.add( element ); + this._on( element, { + mouseenter: function( event ) { + $( event.currentTarget ).addClass( "ui-state-hover" ); + }, + mouseleave: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-hover" ); + } + }); + }, + + _focusable: function( element ) { + this.focusable = this.focusable.add( element ); + this._on( element, { + focusin: function( event ) { + $( event.currentTarget ).addClass( "ui-state-focus" ); + }, + focusout: function( event ) { + $( event.currentTarget ).removeClass( "ui-state-focus" ); + } + }); + }, + + _trigger: function( type, event, data ) { + var prop, orig, + callback = this.options[ type ]; + + data = data || {}; + event = $.Event( event ); + event.type = ( type === this.widgetEventPrefix ? + type : + this.widgetEventPrefix + type ).toLowerCase(); + // the original event may come from any element + // so we need to reset the target on the new event + event.target = this.element[ 0 ]; + + // copy original event properties over to the new event + orig = event.originalEvent; + if ( orig ) { + for ( prop in orig ) { + if ( !( prop in event ) ) { + event[ prop ] = orig[ prop ]; + } + } + } + + this.element.trigger( event, data ); + return !( $.isFunction( callback ) && + callback.apply( this.element[0], [ event ].concat( data ) ) === false || + event.isDefaultPrevented() ); + } + }; + + $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { + $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { + if ( typeof options === "string" ) { + options = { effect: options }; + } + var hasOptions, + effectName = !options ? + method : + options === true || typeof options === "number" ? + defaultEffect : + options.effect || defaultEffect; + options = options || {}; + if ( typeof options === "number" ) { + options = { duration: options }; + } + hasOptions = !$.isEmptyObject( options ); + options.complete = callback; + if ( options.delay ) { + element.delay( options.delay ); + } + if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { + element[ method ]( options ); + } else if ( effectName !== method && element[ effectName ] ) { + element[ effectName ]( options.duration, options.easing, callback ); + } else { + element.queue(function( next ) { + $( this )[ method ](); + if ( callback ) { + callback.call( element[ 0 ] ); + } + next(); + }); + } + }; + }); + + var widget = $.widget; + + + +})); diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js new file mode 100644 index 000000000..54457dab7 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_lunr.js @@ -0,0 +1,1910 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 0.5.7 + * Copyright (C) 2014 Oliver Nightingale + * MIT Licensed + * @license + */ + +(function(){ + + /** + * Convenience function for instantiating a new lunr index and configuring it + * with the default pipeline functions and the passed config function. + * + * When using this convenience function a new index will be created with the + * following functions already in the pipeline: + * + * lunr.StopWordFilter - filters out any stop words before they enter the + * index + * + * lunr.stemmer - stems the tokens before entering the index. + * + * Example: + * + * var idx = lunr(function () { + * this.field('title', 10) + * this.field('tags', 100) + * this.field('body') + * + * this.ref('cid') + * + * this.pipeline.add(function () { + * // some custom pipeline function + * }) + * + * }) + * + * @param {Function} config A function that will be called with the new instance + * of the lunr.Index as both its context and first parameter. It can be used to + * customize the instance of new lunr.Index. + * @namespace + * @module + * @returns {lunr.Index} + * + */ + var lunr = function (config) { + var idx = new lunr.Index + + idx.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + if (config) config.call(idx, idx) + + return idx + } + + lunr.version = "0.5.7" + /*! + * lunr.utils + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * A namespace containing utils for the rest of the lunr library + */ + lunr.utils = {} + + /** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf Utils + */ + lunr.utils.warn = (function (global) { + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } + })(this) + + /*! + * lunr.EventEmitter + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.EventEmitter is an event emitter for lunr. It manages adding and removing event handlers and triggering events and their handlers. + * + * @constructor + */ + lunr.EventEmitter = function () { + this.events = {} + } + + /** + * Binds a handler function to a specific event(s). + * + * Can bind a single function to many different events in one call. + * + * @param {String} [eventName] The name(s) of events to bind this function to. + * @param {Function} handler The function to call when an event is fired. + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.addListener = function () { + var args = Array.prototype.slice.call(arguments), + fn = args.pop(), + names = args + + if (typeof fn !== "function") throw new TypeError ("last argument must be a function") + + names.forEach(function (name) { + if (!this.hasHandler(name)) this.events[name] = [] + this.events[name].push(fn) + }, this) + } + + /** + * Removes a handler function from a specific event. + * + * @param {String} eventName The name of the event to remove this function from. + * @param {Function} handler The function to remove from an event. + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.removeListener = function (name, fn) { + if (!this.hasHandler(name)) return + + var fnIndex = this.events[name].indexOf(fn) + this.events[name].splice(fnIndex, 1) + + if (!this.events[name].length) delete this.events[name] + } + + /** + * Calls all functions bound to the given event. + * + * Additional data can be passed to the event handler as arguments to `emit` + * after the event name. + * + * @param {String} eventName The name of the event to emit. + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.emit = function (name) { + if (!this.hasHandler(name)) return + + var args = Array.prototype.slice.call(arguments, 1) + + this.events[name].forEach(function (fn) { + fn.apply(undefined, args) + }) + } + + /** + * Checks whether a handler has ever been stored against an event. + * + * @param {String} eventName The name of the event to check. + * @private + * @memberOf EventEmitter + */ + lunr.EventEmitter.prototype.hasHandler = function (name) { + return name in this.events + } + + /*! + * lunr.tokenizer + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * A function for splitting a string into tokens ready to be inserted into + * the search index. + * + * @module + * @param {String} obj The string to convert into tokens + * @returns {Array} + */ + lunr.tokenizer = function (obj) { + if (!arguments.length || obj == null || obj == undefined) return [] + if (Array.isArray(obj)) return obj.map(function (t) { return t.toLowerCase() }) + + var str = obj.toString().replace(/^\s+/, '') + + for (var i = str.length - 1; i >= 0; i--) { + if (/\S/.test(str.charAt(i))) { + str = str.substring(0, i + 1) + break + } + } + + return str + .split(/(?:\s+|\-)/) + .filter(function (token) { + return !!token + }) + .map(function (token) { + return token.toLowerCase() + }) + } + /*! + * lunr.Pipeline + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ + lunr.Pipeline = function () { + this._stack = [] + } + + lunr.Pipeline.registeredFunctions = {} + + /** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {Function} fn The function to check for. + * @param {String} label The label to register this function with + * @memberOf Pipeline + */ + lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn + } + + /** + * Warns if the function is not registered as a Pipeline function. + * + * @param {Function} fn The function to check for. + * @private + * @memberOf Pipeline + */ + lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } + } + + /** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised The serialised pipeline to load. + * @returns {lunr.Pipeline} + * @memberOf Pipeline + */ + lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error ('Cannot load un-registered function: ' + fnName) + } + }) + + return pipeline + } + + /** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} functions Any number of functions to add to the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) + } + + /** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + 1 + this._stack.splice(pos, 0, newFn) + } + + /** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {Function} existingFn A function that already exists in the pipeline. + * @param {Function} newFn The new function to add to the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + this._stack.splice(pos, 0, newFn) + } + + /** + * Removes a function from the pipeline. + * + * @param {Function} fn The function to remove from the pipeline. + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + this._stack.splice(pos, 1) + } + + /** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.run = function (tokens) { + var out = [], + tokenLength = tokens.length, + stackLength = this._stack.length + + for (var i = 0; i < tokenLength; i++) { + var token = tokens[i] + + for (var j = 0; j < stackLength; j++) { + token = this._stack[j](token, i, tokens) + if (token === void 0) break + }; + + if (token !== void 0) out.push(token) + }; + + return out + } + + /** + * Resets the pipeline by removing any existing processors. + * + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.reset = function () { + this._stack = [] + } + + /** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + * @memberOf Pipeline + */ + lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) + } + /*! + * lunr.Vector + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Vectors implement vector related operations for + * a series of elements. + * + * @constructor + */ + lunr.Vector = function () { + this._magnitude = null + this.list = undefined + this.length = 0 + } + + /** + * lunr.Vector.Node is a simple struct for each node + * in a lunr.Vector. + * + * @private + * @param {Number} The index of the node in the vector. + * @param {Object} The data at this node in the vector. + * @param {lunr.Vector.Node} The node directly after this node in the vector. + * @constructor + * @memberOf Vector + */ + lunr.Vector.Node = function (idx, val, next) { + this.idx = idx + this.val = val + this.next = next + } + + /** + * Inserts a new value at a position in a vector. + * + * @param {Number} The index at which to insert a value. + * @param {Object} The object to insert in the vector. + * @memberOf Vector. + */ + lunr.Vector.prototype.insert = function (idx, val) { + var list = this.list + + if (!list) { + this.list = new lunr.Vector.Node (idx, val, list) + return this.length++ + } + + var prev = list, + next = list.next + + while (next != undefined) { + if (idx < next.idx) { + prev.next = new lunr.Vector.Node (idx, val, next) + return this.length++ + } + + prev = next, next = next.next + } + + prev.next = new lunr.Vector.Node (idx, val, next) + return this.length++ + } + + /** + * Calculates the magnitude of this vector. + * + * @returns {Number} + * @memberOf Vector + */ + lunr.Vector.prototype.magnitude = function () { + if (this._magniture) return this._magnitude + var node = this.list, + sumOfSquares = 0, + val + + while (node) { + val = node.val + sumOfSquares += val * val + node = node.next + } + + return this._magnitude = Math.sqrt(sumOfSquares) + } + + /** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector The vector to compute the dot product with. + * @returns {Number} + * @memberOf Vector + */ + lunr.Vector.prototype.dot = function (otherVector) { + var node = this.list, + otherNode = otherVector.list, + dotProduct = 0 + + while (node && otherNode) { + if (node.idx < otherNode.idx) { + node = node.next + } else if (node.idx > otherNode.idx) { + otherNode = otherNode.next + } else { + dotProduct += node.val * otherNode.val + node = node.next + otherNode = otherNode.next + } + } + + return dotProduct + } + + /** + * Calculates the cosine similarity between this vector and another + * vector. + * + * @param {lunr.Vector} otherVector The other vector to calculate the + * similarity with. + * @returns {Number} + * @memberOf Vector + */ + lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / (this.magnitude() * otherVector.magnitude()) + } + /*! + * lunr.SortedSet + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.SortedSets are used to maintain an array of uniq values in a sorted + * order. + * + * @constructor + */ + lunr.SortedSet = function () { + this.length = 0 + this.elements = [] + } + + /** + * Loads a previously serialised sorted set. + * + * @param {Array} serialisedData The serialised set to load. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.load = function (serialisedData) { + var set = new this + + set.elements = serialisedData + set.length = serialisedData.length + + return set + } + + /** + * Inserts new items into the set in the correct position to maintain the + * order. + * + * @param {Object} The objects to add to this set. + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.add = function () { + Array.prototype.slice.call(arguments).forEach(function (element) { + if (~this.indexOf(element)) return + this.elements.splice(this.locationFor(element), 0, element) + }, this) + + this.length = this.elements.length + } + + /** + * Converts this sorted set into an array. + * + * @returns {Array} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.toArray = function () { + return this.elements.slice() + } + + /** + * Creates a new array with the results of calling a provided function on every + * element in this sorted set. + * + * Delegates to Array.prototype.map and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * for the function fn. + * @returns {Array} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.map = function (fn, ctx) { + return this.elements.map(fn, ctx) + } + + /** + * Executes a provided function once per sorted set element. + * + * Delegates to Array.prototype.forEach and has the same signature. + * + * @param {Function} fn The function that is called on each element of the + * set. + * @param {Object} ctx An optional object that can be used as the context + * @memberOf SortedSet + * for the function fn. + */ + lunr.SortedSet.prototype.forEach = function (fn, ctx) { + return this.elements.forEach(fn, ctx) + } + + /** + * Returns the index at which a given element can be found in the + * sorted set, or -1 if it is not present. + * + * @param {Object} elem The object to locate in the sorted set. + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.indexOf = function (elem, start, end) { + var start = start || 0, + end = end || this.elements.length, + sectionLength = end - start, + pivot = start + Math.floor(sectionLength / 2), + pivotElem = this.elements[pivot] + + if (sectionLength <= 1) { + if (pivotElem === elem) { + return pivot + } else { + return -1 + } + } + + if (pivotElem < elem) return this.indexOf(elem, pivot, end) + if (pivotElem > elem) return this.indexOf(elem, start, pivot) + if (pivotElem === elem) return pivot + } + + /** + * Returns the position within the sorted set that an element should be + * inserted at to maintain the current order of the set. + * + * This function assumes that the element to search for does not already exist + * in the sorted set. + * + * @param {Object} elem The elem to find the position for in the set + * @param {Number} start An optional index at which to start searching from + * within the set. + * @param {Number} end An optional index at which to stop search from within + * the set. + * @returns {Number} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.locationFor = function (elem, start, end) { + var start = start || 0, + end = end || this.elements.length, + sectionLength = end - start, + pivot = start + Math.floor(sectionLength / 2), + pivotElem = this.elements[pivot] + + if (sectionLength <= 1) { + if (pivotElem > elem) return pivot + if (pivotElem < elem) return pivot + 1 + } + + if (pivotElem < elem) return this.locationFor(elem, pivot, end) + if (pivotElem > elem) return this.locationFor(elem, start, pivot) + } + + /** + * Creates a new lunr.SortedSet that contains the elements in the intersection + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to intersect with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.intersect = function (otherSet) { + var intersectSet = new lunr.SortedSet, + i = 0, j = 0, + a_len = this.length, b_len = otherSet.length, + a = this.elements, b = otherSet.elements + + while (true) { + if (i > a_len - 1 || j > b_len - 1) break + + if (a[i] === b[j]) { + intersectSet.add(a[i]) + i++, j++ + continue + } + + if (a[i] < b[j]) { + i++ + continue + } + + if (a[i] > b[j]) { + j++ + continue + } + }; + + return intersectSet + } + + /** + * Makes a copy of this set + * + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.clone = function () { + var clone = new lunr.SortedSet + + clone.elements = this.toArray() + clone.length = clone.elements.length + + return clone + } + + /** + * Creates a new lunr.SortedSet that contains the elements in the union + * of this set and the passed set. + * + * @param {lunr.SortedSet} otherSet The set to union with this set. + * @returns {lunr.SortedSet} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.union = function (otherSet) { + var longSet, shortSet, unionSet + + if (this.length >= otherSet.length) { + longSet = this, shortSet = otherSet + } else { + longSet = otherSet, shortSet = this + } + + unionSet = longSet.clone() + + unionSet.add.apply(unionSet, shortSet.toArray()) + + return unionSet + } + + /** + * Returns a representation of the sorted set ready for serialisation. + * + * @returns {Array} + * @memberOf SortedSet + */ + lunr.SortedSet.prototype.toJSON = function () { + return this.toArray() + } + /*! + * lunr.Index + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Index is object that manages a search index. It contains the indexes + * and stores all the tokens and document lookups. It also provides the main + * user facing API for the library. + * + * @constructor + */ + lunr.Index = function () { + this._fields = [] + this._ref = 'id' + this.pipeline = new lunr.Pipeline + this.documentStore = new lunr.Store + this.tokenStore = new lunr.TokenStore + this.corpusTokens = new lunr.SortedSet + this.eventEmitter = new lunr.EventEmitter + + this._idfCache = {} + + this.on('add', 'remove', 'update', (function () { + this._idfCache = {} + }).bind(this)) + } + + /** + * Bind a handler to events being emitted by the index. + * + * The handler can be bound to many events at the same time. + * + * @param {String} [eventName] The name(s) of events to bind the function to. + * @param {Function} handler The serialised set to load. + * @memberOf Index + */ + lunr.Index.prototype.on = function () { + var args = Array.prototype.slice.call(arguments) + return this.eventEmitter.addListener.apply(this.eventEmitter, args) + } + + /** + * Removes a handler from an event being emitted by the index. + * + * @param {String} eventName The name of events to remove the function from. + * @param {Function} handler The serialised set to load. + * @memberOf Index + */ + lunr.Index.prototype.off = function (name, fn) { + return this.eventEmitter.removeListener(name, fn) + } + + /** + * Loads a previously serialised index. + * + * Issues a warning if the index being imported was serialised + * by a different version of lunr. + * + * @param {Object} serialisedData The serialised set to load. + * @returns {lunr.Index} + * @memberOf Index + */ + lunr.Index.load = function (serialisedData) { + if (serialisedData.version !== lunr.version) { + lunr.utils.warn('version mismatch: current ' + lunr.version + ' importing ' + serialisedData.version) + } + + var idx = new this + + idx._fields = serialisedData.fields + idx._ref = serialisedData.ref + + idx.documentStore = lunr.Store.load(serialisedData.documentStore) + idx.tokenStore = lunr.TokenStore.load(serialisedData.tokenStore) + idx.corpusTokens = lunr.SortedSet.load(serialisedData.corpusTokens) + idx.pipeline = lunr.Pipeline.load(serialisedData.pipeline) + + return idx + } + + /** + * Adds a field to the list of fields that will be searchable within documents + * in the index. + * + * An optional boost param can be passed to affect how much tokens in this field + * rank in search results, by default the boost value is 1. + * + * Fields should be added before any documents are added to the index, fields + * that are added after documents are added to the index will only apply to new + * documents added to the index. + * + * @param {String} fieldName The name of the field within the document that + * should be indexed + * @param {Number} boost An optional boost that can be applied to terms in this + * field. + * @returns {lunr.Index} + * @memberOf Index + */ + lunr.Index.prototype.field = function (fieldName, opts) { + var opts = opts || {}, + field = { name: fieldName, boost: opts.boost || 1 } + + this._fields.push(field) + return this + } + + /** + * Sets the property used to uniquely identify documents added to the index, + * by default this property is 'id'. + * + * This should only be changed before adding documents to the index, changing + * the ref property without resetting the index can lead to unexpected results. + * + * @param {String} refName The property to use to uniquely identify the + * documents in the index. + * @param {Boolean} emitEvent Whether to emit add events, defaults to true + * @returns {lunr.Index} + * @memberOf Index + */ + lunr.Index.prototype.ref = function (refName) { + this._ref = refName + return this + } + + /** + * Add a document to the index. + * + * This is the way new documents enter the index, this function will run the + * fields from the document through the index's pipeline and then add it to + * the index, it will then show up in search results. + * + * An 'add' event is emitted with the document that has been added and the index + * the document has been added to. This event can be silenced by passing false + * as the second argument to add. + * + * @param {Object} doc The document to add to the index. + * @param {Boolean} emitEvent Whether or not to emit events, default true. + * @memberOf Index + */ + lunr.Index.prototype.add = function (doc, emitEvent) { + var docTokens = {}, + allDocumentTokens = new lunr.SortedSet, + docRef = doc[this._ref], + emitEvent = emitEvent === undefined ? true : emitEvent + + this._fields.forEach(function (field) { + var fieldTokens = this.pipeline.run(lunr.tokenizer(doc[field.name])) + + docTokens[field.name] = fieldTokens + lunr.SortedSet.prototype.add.apply(allDocumentTokens, fieldTokens) + }, this) + + this.documentStore.set(docRef, allDocumentTokens) + lunr.SortedSet.prototype.add.apply(this.corpusTokens, allDocumentTokens.toArray()) + + for (var i = 0; i < allDocumentTokens.length; i++) { + var token = allDocumentTokens.elements[i] + var tf = this._fields.reduce(function (memo, field) { + var fieldLength = docTokens[field.name].length + + if (!fieldLength) return memo + + var tokenCount = docTokens[field.name].filter(function (t) { return t === token }).length + + return memo + (tokenCount / fieldLength * field.boost) + }, 0) + + this.tokenStore.add(token, { ref: docRef, tf: tf }) + }; + + if (emitEvent) this.eventEmitter.emit('add', doc, this) + } + + /** + * Removes a document from the index. + * + * To make sure documents no longer show up in search results they can be + * removed from the index using this method. + * + * The document passed only needs to have the same ref property value as the + * document that was added to the index, they could be completely different + * objects. + * + * A 'remove' event is emitted with the document that has been removed and the index + * the document has been removed from. This event can be silenced by passing false + * as the second argument to remove. + * + * @param {Object} doc The document to remove from the index. + * @param {Boolean} emitEvent Whether to emit remove events, defaults to true + * @memberOf Index + */ + lunr.Index.prototype.remove = function (doc, emitEvent) { + var docRef = doc[this._ref], + emitEvent = emitEvent === undefined ? true : emitEvent + + if (!this.documentStore.has(docRef)) return + + var docTokens = this.documentStore.get(docRef) + + this.documentStore.remove(docRef) + + docTokens.forEach(function (token) { + this.tokenStore.remove(token, docRef) + }, this) + + if (emitEvent) this.eventEmitter.emit('remove', doc, this) + } + + /** + * Updates a document in the index. + * + * When a document contained within the index gets updated, fields changed, + * added or removed, to make sure it correctly matched against search queries, + * it should be updated in the index. + * + * This method is just a wrapper around `remove` and `add` + * + * An 'update' event is emitted with the document that has been updated and the index. + * This event can be silenced by passing false as the second argument to update. Only + * an update event will be fired, the 'add' and 'remove' events of the underlying calls + * are silenced. + * + * @param {Object} doc The document to update in the index. + * @param {Boolean} emitEvent Whether to emit update events, defaults to true + * @see Index.prototype.remove + * @see Index.prototype.add + * @memberOf Index + */ + lunr.Index.prototype.update = function (doc, emitEvent) { + var emitEvent = emitEvent === undefined ? true : emitEvent + + this.remove(doc, false) + this.add(doc, false) + + if (emitEvent) this.eventEmitter.emit('update', doc, this) + } + + /** + * Calculates the inverse document frequency for a token within the index. + * + * @param {String} token The token to calculate the idf of. + * @see Index.prototype.idf + * @private + * @memberOf Index + */ + lunr.Index.prototype.idf = function (term) { + var cacheKey = "@" + term + if (Object.prototype.hasOwnProperty.call(this._idfCache, cacheKey)) return this._idfCache[cacheKey] + + var documentFrequency = this.tokenStore.count(term), + idf = 1 + + if (documentFrequency > 0) { + idf = 1 + Math.log(this.tokenStore.length / documentFrequency) + } + + return this._idfCache[cacheKey] = idf + } + + /** + * Searches the index using the passed query. + * + * Queries should be a string, multiple words are allowed and will lead to an + * AND based query, e.g. `idx.search('foo bar')` will run a search for + * documents containing both 'foo' and 'bar'. + * + * All query tokens are passed through the same pipeline that document tokens + * are passed through, so any language processing involved will be run on every + * query term. + * + * Each query term is expanded, so that the term 'he' might be expanded to + * 'hello' and 'help' if those terms were already included in the index. + * + * Matching documents are returned as an array of objects, each object contains + * the matching document ref, as set for this index, and the similarity score + * for this document against the query. + * + * @param {String} query The query to search the index with. + * @returns {Object} + * @see Index.prototype.idf + * @see Index.prototype.documentVector + * @memberOf Index + */ + lunr.Index.prototype.search = function (query) { + var queryTokens = this.pipeline.run(lunr.tokenizer(query)), + queryVector = new lunr.Vector, + documentSets = [], + fieldBoosts = this._fields.reduce(function (memo, f) { return memo + f.boost }, 0) + + var hasSomeToken = queryTokens.some(function (token) { + return this.tokenStore.has(token) + }, this) + + if (!hasSomeToken) return [] + + queryTokens + .forEach(function (token, i, tokens) { + var tf = 1 / tokens.length * this._fields.length * fieldBoosts, + self = this + + var set = this.tokenStore.expand(token).reduce(function (memo, key) { + var pos = self.corpusTokens.indexOf(key), + idf = self.idf(key), + similarityBoost = 1, + set = new lunr.SortedSet + + // if the expanded key is not an exact match to the token then + // penalise the score for this key by how different the key is + // to the token. + if (key !== token) { + var diff = Math.max(3, key.length - token.length) + similarityBoost = 1 / Math.log(diff) + } + + // calculate the query tf-idf score for this token + // applying an similarityBoost to ensure exact matches + // these rank higher than expanded terms + if (pos > -1) queryVector.insert(pos, tf * idf * similarityBoost) + + // add all the documents that have this key into a set + Object.keys(self.tokenStore.get(key)).forEach(function (ref) { set.add(ref) }) + + return memo.union(set) + }, new lunr.SortedSet) + + documentSets.push(set) + }, this) + + var documentSet = documentSets.reduce(function (memo, set) { + return memo.intersect(set) + }) + + return documentSet + .map(function (ref) { + return { ref: ref, score: queryVector.similarity(this.documentVector(ref)) } + }, this) + .sort(function (a, b) { + return b.score - a.score + }) + } + + /** + * Generates a vector containing all the tokens in the document matching the + * passed documentRef. + * + * The vector contains the tf-idf score for each token contained in the + * document with the passed documentRef. The vector will contain an element + * for every token in the indexes corpus, if the document does not contain that + * token the element will be 0. + * + * @param {Object} documentRef The ref to find the document with. + * @returns {lunr.Vector} + * @private + * @memberOf Index + */ + lunr.Index.prototype.documentVector = function (documentRef) { + var documentTokens = this.documentStore.get(documentRef), + documentTokensLength = documentTokens.length, + documentVector = new lunr.Vector + + for (var i = 0; i < documentTokensLength; i++) { + var token = documentTokens.elements[i], + tf = this.tokenStore.get(token)[documentRef].tf, + idf = this.idf(token) + + documentVector.insert(this.corpusTokens.indexOf(token), tf * idf) + }; + + return documentVector + } + + /** + * Returns a representation of the index ready for serialisation. + * + * @returns {Object} + * @memberOf Index + */ + lunr.Index.prototype.toJSON = function () { + return { + version: lunr.version, + fields: this._fields, + ref: this._ref, + documentStore: this.documentStore.toJSON(), + tokenStore: this.tokenStore.toJSON(), + corpusTokens: this.corpusTokens.toJSON(), + pipeline: this.pipeline.toJSON() + } + } + + /** + * Applies a plugin to the current index. + * + * A plugin is a function that is called with the index as its context. + * Plugins can be used to customise or extend the behaviour the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied to the index. + * + * The plugin function will be called with the index as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index as its context. + * + * Example: + * + * var myPlugin = function (idx, arg1, arg2) { + * // `this` is the index to be extended + * // apply any extensions etc here. + * } + * + * var idx = lunr(function () { + * this.use(myPlugin, 'arg1', 'arg2') + * }) + * + * @param {Function} plugin The plugin to apply. + * @memberOf Index + */ + lunr.Index.prototype.use = function (plugin) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + plugin.apply(this, args) + } + /*! + * lunr.Store + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.Store is a simple key-value store used for storing sets of tokens for + * documents stored in index. + * + * @constructor + * @module + */ + lunr.Store = function () { + this.store = {} + this.length = 0 + } + + /** + * Loads a previously serialised store + * + * @param {Object} serialisedData The serialised store to load. + * @returns {lunr.Store} + * @memberOf Store + */ + lunr.Store.load = function (serialisedData) { + var store = new this + + store.length = serialisedData.length + store.store = Object.keys(serialisedData.store).reduce(function (memo, key) { + memo[key] = lunr.SortedSet.load(serialisedData.store[key]) + return memo + }, {}) + + return store + } + + /** + * Stores the given tokens in the store against the given id. + * + * @param {Object} id The key used to store the tokens against. + * @param {Object} tokens The tokens to store against the key. + * @memberOf Store + */ + lunr.Store.prototype.set = function (id, tokens) { + if (!this.has(id)) this.length++ + this.store[id] = tokens + } + + /** + * Retrieves the tokens from the store for a given key. + * + * @param {Object} id The key to lookup and retrieve from the store. + * @returns {Object} + * @memberOf Store + */ + lunr.Store.prototype.get = function (id) { + return this.store[id] + } + + /** + * Checks whether the store contains a key. + * + * @param {Object} id The id to look up in the store. + * @returns {Boolean} + * @memberOf Store + */ + lunr.Store.prototype.has = function (id) { + return id in this.store + } + + /** + * Removes the value for a key in the store. + * + * @param {Object} id The id to remove from the store. + * @memberOf Store + */ + lunr.Store.prototype.remove = function (id) { + if (!this.has(id)) return + + delete this.store[id] + this.length-- + } + + /** + * Returns a representation of the store ready for serialisation. + * + * @returns {Object} + * @memberOf Store + */ + lunr.Store.prototype.toJSON = function () { + return { + store: this.store, + length: this.length + } + } + + /*! + * lunr.stemmer + * Copyright (C) 2014 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + + /** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartaurs.org/~martin + * + * @module + * @param {String} str The string to stem + * @returns {String} + * @see lunr.Pipeline + */ + lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return porterStemmer; + })(); + + lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') + /*! + * lunr.stopWordFilter + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @module + * @param {String} token The token to pass through the filter + * @returns {String} + * @see lunr.Pipeline + */ + lunr.stopWordFilter = function (token) { + if (lunr.stopWordFilter.stopWords.indexOf(token) === -1) return token + } + + lunr.stopWordFilter.stopWords = new lunr.SortedSet + lunr.stopWordFilter.stopWords.length = 119 + lunr.stopWordFilter.stopWords.elements = [ + "", + "a", + "able", + "about", + "across", + "after", + "all", + "almost", + "also", + "am", + "among", + "an", + "and", + "any", + "are", + "as", + "at", + "be", + "because", + "been", + "but", + "by", + "can", + "cannot", + "could", + "dear", + "did", + "do", + "does", + "either", + "else", + "ever", + "every", + "for", + "from", + "get", + "got", + "had", + "has", + "have", + "he", + "her", + "hers", + "him", + "his", + "how", + "however", + "i", + "if", + "in", + "into", + "is", + "it", + "its", + "just", + "least", + "let", + "like", + "likely", + "may", + "me", + "might", + "most", + "must", + "my", + "neither", + "no", + "nor", + "not", + "of", + "off", + "often", + "on", + "only", + "or", + "other", + "our", + "own", + "rather", + "said", + "say", + "says", + "she", + "should", + "since", + "so", + "some", + "than", + "that", + "the", + "their", + "them", + "then", + "there", + "these", + "they", + "this", + "tis", + "to", + "too", + "twas", + "us", + "wants", + "was", + "we", + "were", + "what", + "when", + "where", + "which", + "while", + "who", + "whom", + "why", + "will", + "with", + "would", + "yet", + "you", + "your" + ] + + lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') + /*! + * lunr.trimmer + * Copyright (C) 2014 Oliver Nightingale + */ + + /** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the begining and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @module + * @param {String} token The token to pass through the filter + * @returns {String} + * @see lunr.Pipeline + */ + lunr.trimmer = function (token) { + return token + .replace(/^\W+/, '') + .replace(/\W+$/, '') + } + + lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') + /*! + * lunr.stemmer + * Copyright (C) 2014 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + + /** + * lunr.TokenStore is used for efficient storing and lookup of the reverse + * index of token to document ref. + * + * @constructor + */ + lunr.TokenStore = function () { + this.root = { docs: {} } + this.length = 0 + } + + /** + * Loads a previously serialised token store + * + * @param {Object} serialisedData The serialised token store to load. + * @returns {lunr.TokenStore} + * @memberOf TokenStore + */ + lunr.TokenStore.load = function (serialisedData) { + var store = new this + + store.root = serialisedData.root + store.length = serialisedData.length + + return store + } + + /** + * Adds a new token doc pair to the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to store the doc under + * @param {Object} doc The doc to store against the token + * @param {Object} root An optional node at which to start looking for the + * correct place to enter the doc, by default the root of this lunr.TokenStore + * is used. + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.add = function (token, doc, root) { + var root = root || this.root, + key = token[0], + rest = token.slice(1) + + if (!(key in root)) root[key] = {docs: {}} + + if (rest.length === 0) { + root[key].docs[doc.ref] = doc + this.length += 1 + return + } else { + return this.add(rest, doc, root[key]) + } + } + + /** + * Checks whether this key is contained within this lunr.TokenStore. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to check for + * @param {Object} root An optional node at which to start + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.has = function (token) { + if (!token) return false + + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!node[token[i]]) return false + + node = node[token[i]] + } + + return true + } + + /** + * Retrieve a node from the token store for a given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the node for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @see TokenStore.prototype.get + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.getNode = function (token) { + if (!token) return {} + + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!node[token[i]]) return {} + + node = node[token[i]] + } + + return node + } + + /** + * Retrieve the documents for a node for the given token. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.get = function (token, root) { + return this.getNode(token, root).docs || {} + } + + lunr.TokenStore.prototype.count = function (token, root) { + return Object.keys(this.get(token, root)).length + } + + /** + * Remove the document identified by ref from the token in the store. + * + * By default this function starts at the root of the current store, however + * it can start at any node of any token store if required. + * + * @param {String} token The token to get the documents for. + * @param {String} ref The ref of the document to remove from this token. + * @param {Object} root An optional node at which to start. + * @returns {Object} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.remove = function (token, ref) { + if (!token) return + var node = this.root + + for (var i = 0; i < token.length; i++) { + if (!(token[i] in node)) return + node = node[token[i]] + } + + delete node.docs[ref] + } + + /** + * Find all the possible suffixes of the passed token using tokens + * currently in the store. + * + * @param {String} token The token to expand. + * @returns {Array} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.expand = function (token, memo) { + var root = this.getNode(token), + docs = root.docs || {}, + memo = memo || [] + + if (Object.keys(docs).length) memo.push(token) + + Object.keys(root) + .forEach(function (key) { + if (key === 'docs') return + + memo.concat(this.expand(token + key, memo)) + }, this) + + return memo + } + + /** + * Returns a representation of the token store ready for serialisation. + * + * @returns {Object} + * @memberOf TokenStore + */ + lunr.TokenStore.prototype.toJSON = function () { + return { + root: this.root, + length: this.length + } + } + + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like enviroments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})() diff --git a/samples/rest-notes-slate/slate/source/layouts/layout.erb b/samples/rest-notes-slate/slate/source/layouts/layout.erb new file mode 100644 index 000000000..36ae0f95f --- /dev/null +++ b/samples/rest-notes-slate/slate/source/layouts/layout.erb @@ -0,0 +1,102 @@ +<%# +Copyright 2008-2013 Concur Technologies, Inc. + +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. +%> +<% language_tabs = current_page.data.language_tabs %> + + + + + + + <%= current_page.data.title || "API Documentation" %> + + <%= stylesheet_link_tag :screen, media: :screen %> + <%= stylesheet_link_tag :print, media: :print %> + + <% if current_page.data.search %> + <%= javascript_include_tag "all" %> + <% else %> + <%= javascript_include_tag "all_nosearch" %> + <% end %> + + <% if language_tabs %> + + <% end %> + + + + + + NAV + <%= image_tag('navbar.png') %> + + +
        + <%= image_tag "logo.png" %> + <% if language_tabs %> +
        + <% language_tabs.each do |lang| %> + <% if lang.is_a? Hash %> + <%= lang.values.first %> + <% else %> + <%= lang %> + <% end %> + <% end %> +
        + <% end %> + <% if current_page.data.search %> + +
          + <% end %> +
          +
          + <% if current_page.data.toc_footers %> + + <% end %> +
          +
          +
          +
          + <%= yield %> + <% current_page.data.includes && current_page.data.includes.each do |include| %> + <%= partial "includes/#{include}" %> + <% end %> +
          +
          + <% if language_tabs %> +
          + <% language_tabs.each do |lang| %> + <% if lang.is_a? Hash %> + <%= lang.values.first %> + <% else %> + <%= lang %> + <% end %> + <% end %> +
          + <% end %> +
          +
          + + diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss b/samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss new file mode 100644 index 000000000..b59948398 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/_icon-font.scss @@ -0,0 +1,38 @@ +@font-face { + font-family: 'slate'; + src:font-url('slate.eot?-syv14m'); + src:font-url('slate.eot?#iefix-syv14m') format('embedded-opentype'), + font-url('slate.woff2?-syv14m') format('woff2'), + font-url('slate.woff?-syv14m') format('woff'), + font-url('slate.ttf?-syv14m') format('truetype'), + font-url('slate.svg?-syv14m#slate') format('svg'); + font-weight: normal; + font-style: normal; +} + +%icon { + font-family: 'slate'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; +} + +%icon-exclamation-sign { + @extend %icon; + content: "\e600"; +} +%icon-info-sign { + @extend %icon; + content: "\e602"; +} +%icon-ok-sign { + @extend %icon; + content: "\e606"; +} +%icon-search { + @extend %icon; + content: "\e607"; +} diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_normalize.css b/samples/rest-notes-slate/slate/source/stylesheets/_normalize.css new file mode 100644 index 000000000..46f646a5c --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/_normalize.css @@ -0,0 +1,427 @@ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ + +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Remove default margin. + */ + +body { + margin: 0; +} + +/* HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ + +audio, +canvas, +progress, +video { + display: inline-block; /* 1 */ + vertical-align: baseline; /* 2 */ +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. + */ + +[hidden], +template { + display: none; +} + +/* Links + ========================================================================== */ + +/** + * Remove the gray background color from active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/** + * Address styling not present in Safari and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/** + * Address styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove border when inside `a` element in IE 8/9/10. + */ + +img { + border: 0; +} + +/** + * Correct overflow not hidden in IE 9/10/11. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* Grouping content + ========================================================================== */ + +/** + * Address margin not present in IE 8/9 and Safari. + */ + +figure { + margin: 1em 40px; +} + +/** + * Address differences between Firefox and other browsers. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Contain overflow in all browsers. + */ + +pre { + overflow: auto; +} + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +/* Forms + ========================================================================== */ + +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ + +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ + +button, +input, +optgroup, +select, +textarea { + color: inherit; /* 1 */ + font: inherit; /* 2 */ + margin: 0; /* 3 */ +} + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ + +button { + overflow: visible; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * Remove inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +input { + line-height: normal; +} + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ + +textarea { + overflow: auto; +} + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ + +optgroup { + font-weight: bold; +} + +/* Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} + +td, +th { + padding: 0; +} diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb b/samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb new file mode 100644 index 000000000..dfeb0c152 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb @@ -0,0 +1,27 @@ +/* +Copyright 2008-2013 Concur Technologies, Inc. + +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. +*/ + +@import 'variables'; + +<%= Rouge::Themes::Base16::Monokai.render(:scope => '.highlight') %> + +.highlight .c, .highlight .cm, .highlight .c1, .highlight .cs { + color: #909090; +} + +.highlight, .highlight .w { + background-color: $code-bg; +} \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/stylesheets/_variables.scss b/samples/rest-notes-slate/slate/source/stylesheets/_variables.scss new file mode 100644 index 000000000..5fe64b1f3 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/_variables.scss @@ -0,0 +1,109 @@ +/* +Copyright 2008-2013 Concur Technologies, Inc. + +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. +*/ + + +//////////////////////////////////////////////////////////////////////////////// +// CUSTOMIZE SLATE +//////////////////////////////////////////////////////////////////////////////// +// Use these settings to help adjust the appearance of Slate + + +// BACKGROUND COLORS +//////////////////// +$nav-bg: #393939; +$examples-bg: #393939; +$code-bg: #292929; +$code-annotation-bg: #1c1c1c; +$nav-subitem-bg: #262626; +$nav-active-bg: #2467af; +$lang-select-border: #000; +$lang-select-bg: #222; +$lang-select-active-bg: $examples-bg; // feel free to change this to blue or something +$lang-select-pressed-bg: #111; // color of language tab bg when mouse is pressed +$main-bg: #eaf2f6; +$aside-notice-bg: #8fbcd4; +$aside-warning-bg: #c97a7e; +$aside-success-bg: #6ac174; +$search-notice-bg: #c97a7e; + + +// TEXT COLORS +//////////////////// +$main-text: #333; // main content text color +$nav-text: #fff; +$nav-active-text: #fff; +$lang-select-text: #fff; // color of unselected language tab text +$lang-select-active-text: #fff; // color of selected language tab text +$lang-select-pressed-text: #fff; // color of language tab text when mouse is pressed + + +// SIZES +//////////////////// +$nav-width: 230px; // width of the navbar +$examples-width: 50%; // portion of the screen taken up by code examples +$logo-margin: 20px; // margin between nav items and logo, ignored if search is active +$main-padding: 28px; // padding to left and right of content & examples +$nav-padding: 15px; // padding to left and right of navbar +$nav-v-padding: 10px; // padding used vertically around search boxes and results +$nav-indent: 10px; // extra padding for ToC subitems +$code-annotation-padding: 13px; // padding inside code annotations +$h1-margin-bottom: 21px; // padding under the largest header tags +$tablet-width: 930px; // min width before reverting to tablet size +$phone-width: $tablet-width - $nav-width; // min width before reverting to mobile size + + +// FONTS +//////////////////// +%default-font { + font-family: "Helvetica Neue", Helvetica, Arial, "Microsoft Yahei","微软雅黑", STXihei, "华文细黑", sans-serif; + font-size: 13px; +} + +%header-font { + @extend %default-font; + font-weight: bold; +} + +%code-font { + font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace, serif; + font-size: 12px; + line-height: 1.5; +} + + +// OTHER +//////////////////// +$nav-active-shadow: #000; +$nav-footer-border-color: #666; +$nav-embossed-border-top: #000; +$nav-embossed-border-bottom: #939393; +$main-embossed-text-shadow: 0px 1px 0px #fff; +$search-box-border-color: #666; + + +//////////////////////////////////////////////////////////////////////////////// +// INTERNAL +//////////////////////////////////////////////////////////////////////////////// +// These settings are probably best left alone. + +%break-words { + word-break: break-all; + + /* Non standard for webkit */ + word-break: break-word; + + hyphens: auto; +} diff --git a/samples/rest-notes-slate/slate/source/stylesheets/print.css.scss b/samples/rest-notes-slate/slate/source/stylesheets/print.css.scss new file mode 100644 index 000000000..4bda057f0 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/print.css.scss @@ -0,0 +1,142 @@ +@charset "utf-8"; +@import 'normalize'; +@import 'compass'; +@import 'variables'; +@import 'icon-font'; + +/* +Copyright 2008-2013 Concur Technologies, Inc. + +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. +*/ + +$print-color: #999; +$print-color-light: #ccc; +$print-font-size: 12px; + +body { + @extend %default-font; +} + +.tocify, .toc-footer, .lang-selector, .search, #nav-button { + display: none; +} + +.tocify-wrapper>img { + margin: 0 auto; + display: block; +} + +.content { + font-size: 12px; + + pre, code { + @extend %code-font; + @extend %break-words; + border: 1px solid $print-color; + border-radius: 5px; + font-size: 0.8em; + } + + pre { + padding: 1.3em; + } + + code { + padding: 0.2em; + } + + table { + border: 1px solid $print-color; + tr { + border-bottom: 1px solid $print-color; + } + td,th { + padding: 0.7em; + } + } + + p { + line-height: 1.5; + } + + a { + text-decoration: none; + color: #000; + } + + h1 { + @extend %header-font; + font-size: 2.5em; + padding-top: 0.5em; + padding-bottom: 0.5em; + margin-top: 1em; + margin-bottom: $h1-margin-bottom; + border: 2px solid $print-color-light; + border-width: 2px 0; + text-align: center; + } + + h2 { + @extend %header-font; + font-size: 1.8em; + margin-top: 2em; + border-top: 2px solid $print-color-light; + padding-top: 0.8em; + } + + h1+h2, h1+div+h2 { + border-top: none; + padding-top: 0; + margin-top: 0; + } + + h3, h4 { + @extend %header-font; + font-size: 0.8em; + margin-top: 1.5em; + margin-bottom: 0.8em; + text-transform: uppercase; + } + + h5, h6 { + text-transform: uppercase; + } + + aside { + padding: 1em; + border: 1px solid $print-color-light; + border-radius: 5px; + margin-top: 1.5em; + margin-bottom: 1.5em; + line-height: 1.6; + } + + aside:before { + vertical-align: middle; + padding-right: 0.5em; + font-size: 14px; + } + + aside.notice:before { + @extend %icon-info-sign; + } + + aside.warning:before { + @extend %icon-exclamation-sign; + } + + aside.success:before { + @extend %icon-ok-sign; + } +} \ No newline at end of file diff --git a/samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss b/samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss new file mode 100644 index 000000000..e4b3ef82b --- /dev/null +++ b/samples/rest-notes-slate/slate/source/stylesheets/screen.css.scss @@ -0,0 +1,620 @@ +@charset "utf-8"; +@import 'normalize'; +@import 'compass'; +@import 'variables'; +@import 'syntax'; +@import 'icon-font'; + +/* +Copyright 2008-2013 Concur Technologies, Inc. + +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. +*/ + +//////////////////////////////////////////////////////////////////////////////// +// GENERAL STUFF +//////////////////////////////////////////////////////////////////////////////// + +html, body { + color: $main-text; + padding: 0; + margin: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + @extend %default-font; + background-color: $main-bg; + height: 100%; + -webkit-text-size-adjust: none; /* Never autoresize text */ +} + +//////////////////////////////////////////////////////////////////////////////// +// TABLE OF CONTENTS +//////////////////////////////////////////////////////////////////////////////// + +#toc > ul > li > a > span { + float: right; + background-color: #2484FF; + border-radius: 40px; + width: 20px; +} + +@mixin embossed-bg { + background: + linear-gradient(to bottom, rgba(#000, 0.2), rgba(#000, 0) 8px), + linear-gradient(to top, rgba(#000, 0.2), rgba(#000, 0) 8px), + linear-gradient(to bottom, rgba($nav-embossed-border-top, 1), rgba($nav-embossed-border-top, 0) 1.5px), + linear-gradient(to top, rgba($nav-embossed-border-bottom, 1), rgba($nav-embossed-border-bottom, 0) 1.5px), + $nav-subitem-bg; +} + +.tocify-wrapper { + transition: left 0.3s ease-in-out; + + overflow-y: auto; + overflow-x: hidden; + position: fixed; + z-index: 30; + top: 0; + left: 0; + bottom: 0; + width: $nav-width; + background-color: $nav-bg; + font-size: 13px; + font-weight: bold; + + // language selector for mobile devices + .lang-selector { + display: none; + a { + padding-top: 0.5em; + padding-bottom: 0.5em; + } + } + + // This is the logo at the top of the ToC + &>img { + display: block; + } + + &>.search { + position: relative; + + input { + background: $nav-bg; + border-width: 0 0 1px 0; + border-color: $search-box-border-color; + padding: 6px 0 6px 20px; + box-sizing: border-box; + margin: $nav-v-padding $nav-padding; + width: $nav-width - 30; + outline: none; + color: $nav-text; + border-radius: 0; /* ios has a default border radius */ + } + + &:before { + position: absolute; + top: 17px; + left: $nav-padding; + color: $nav-text; + @extend %icon-search; + } + } + + img+.tocify { + margin-top: $logo-margin; + } + + .search-results { + margin-top: 0; + box-sizing: border-box; + height: 0; + overflow-y: auto; + overflow-x: hidden; + transition-property: height, margin; + transition-duration: 180ms; + transition-timing-function: ease-in-out; + &.visible { + height: 30%; + margin-bottom: 1em; + } + + @include embossed-bg; + + li { + margin: 1em $nav-padding; + line-height: 1; + } + + a { + color: $nav-text; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + } + + + .tocify-item>a, .toc-footer li { + padding: 0 $nav-padding 0 $nav-padding; + display: block; + overflow-x: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + // The Table of Contents is composed of multiple nested + // unordered lists. These styles remove the default + // styling of an unordered list because it is ugly. + ul, li { + list-style: none; + margin: 0; + padding: 0; + line-height: 28px; + } + + li { + color: $nav-text; + transition-property: background; + transition-timing-function: linear; + transition-duration: 230ms; + } + + // This is the currently selected ToC entry + .tocify-focus { + box-shadow: 0px 1px 0px $nav-active-shadow; + background-color: $nav-active-bg; + color: $nav-active-text; + } + + // Subheaders are the submenus that slide open + // in the table of contents. + .tocify-subheader { + display: none; // tocify will override this when needed + background-color: $nav-subitem-bg; + font-weight: 500; + .tocify-item>a { + padding-left: $nav-padding + $nav-indent; + font-size: 12px; + } + + // for embossed look: + @include embossed-bg; + &>li:last-child { + box-shadow: none; // otherwise it'll overflow out of the subheader + } + } + + .toc-footer { + padding: 1em 0; + margin-top: 1em; + border-top: 1px dashed $nav-footer-border-color; + + li,a { + color: $nav-text; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + + li { + font-size: 0.8em; + line-height: 1.7; + text-decoration: none; + } + } + +} + +// button to show navigation on mobile devices +#nav-button { + span { + display: block; + $side-pad: $main-padding / 2 - 8px; + padding: $side-pad $side-pad $side-pad; + background-color: rgba($main-bg, 0.7); + transform-origin: 0 0; + transform: rotate(-90deg) translate(-100%, 0); + border-radius: 0 0 0 5px; + } + padding: 0 1.5em 5em 0; // increase touch size area + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 100; + color: #000; + text-decoration: none; + font-weight: bold; + opacity: 0.7; + line-height: 16px; + img { + height: 16px; + vertical-align: bottom; + } + + transition: left 0.3s ease-in-out; + + &:hover { opacity: 1; } + &.open {left: $nav-width} +} + + +//////////////////////////////////////////////////////////////////////////////// +// PAGE LAYOUT AND CODE SAMPLE BACKGROUND +//////////////////////////////////////////////////////////////////////////////// + +.page-wrapper { + margin-left: $nav-width; + position: relative; + z-index: 10; + background-color: $main-bg; + min-height: 100%; + + padding-bottom: 1px; // prevent margin overflow + + // The dark box is what gives the code samples their dark background. + // It sits essentially under the actual content block, which has a + // transparent background. + // I know, it's hackish, but it's the simplist way to make the left + // half of the content always this background color. + .dark-box { + width: $examples-width; + background-color: $examples-bg; + position: absolute; + right: 0; + top: 0; + bottom: 0; + } + + .lang-selector { + position: fixed; + z-index: 50; + border-bottom: 5px solid $lang-select-active-bg; + } +} + +.lang-selector { + background-color: $lang-select-bg; + width: 100%; + font-weight: bold; + a { + display: block; + float:left; + color: $lang-select-text; + text-decoration: none; + padding: 0 10px; + line-height: 30px; + outline: 0; + + &:active, &:focus { + background-color: $lang-select-pressed-bg; + color: $lang-select-pressed-text; + } + + &.active { + background-color: $lang-select-active-bg; + color: $lang-select-active-text; + } + } + + &:after { + content: ''; + clear: both; + display: block; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CONTENT STYLES +//////////////////////////////////////////////////////////////////////////////// +// This is all the stuff with the light background in the left half of the page + +.content { + // to place content above the dark box + position: relative; + z-index: 30; + + &:after { + content: ''; + display: block; + clear: both; + } + + &>h1, &>h2, &>h3, &>h4, &>h5, &>h6, &>p, &>table, &>ul, &>ol, &>aside, &>dl { + margin-right: $examples-width; + padding: 0 $main-padding; + box-sizing: border-box; + display: block; + @include text-shadow($main-embossed-text-shadow); + + @extend %left-col; + } + + &>ul, &>ol { + padding-left: $main-padding + 15px; + } + + // the div is the tocify hidden div for placeholding stuff + &>h1, &>h2, &>div { + clear:both; + } + + h1 { + @extend %header-font; + font-size: 30px; + padding-top: 0.5em; + padding-bottom: 0.5em; + border-bottom: 1px solid #ccc; + margin-bottom: $h1-margin-bottom; + margin-top: 2em; + border-top: 1px solid #ddd; + background-image: linear-gradient(to bottom, #fff, #f9f9f9); + } + + h1:first-child, div:first-child + h1 { + border-top-width: 0; + margin-top: 0; + } + + h2 { + @extend %header-font; + font-size: 20px; + margin-top: 4em; + margin-bottom: 0; + border-top: 1px solid #ccc; + padding-top: 1.2em; + padding-bottom: 1.2em; + background-image: linear-gradient(to bottom, rgba(#fff, 0.4), rgba(#fff, 0)); + } + + // h2s right after h1s should bump right up + // against the h1s. + h1 + h2, h1 + div + h2 { + margin-top: $h1-margin-bottom * -1; + border-top: none; + } + + h3, h4, h5, h6 { + @extend %header-font; + font-size: 15px; + margin-top: 2.5em; + margin-bottom: 0.8em; + } + + h4, h5, h6 { + font-size: 10px; + } + + hr { + margin: 2em 0; + border-top: 2px solid $examples-bg; + border-bottom: 2px solid $main-bg; + } + + table { + margin-bottom: 1em; + overflow: auto; + th,td { + text-align: left; + vertical-align: top; + line-height: 1.6; + } + + th { + padding: 5px 10px; + border-bottom: 1px solid #ccc; + vertical-align: bottom; + } + + td { + padding: 10px; + } + + tr:last-child { + border-bottom: 1px solid #ccc; + } + + tr:nth-child(odd)>td { + background-color: lighten($main-bg,4.2%); + } + + tr:nth-child(even)>td { + background-color: lighten($main-bg,2.4%); + } + } + + dt { + font-weight: bold; + } + + dd { + margin-left: 15px; + } + + p, li, dt, dd { + line-height: 1.6; + margin-top: 0; + } + + img { + max-width: 100%; + } + + code { + background-color: rgba(0,0,0,0.05); + padding: 3px; + border-radius: 3px; + @extend %break-words; + @extend %code-font; + } + + pre>code { + background-color: transparent; + padding: 0; + } + + aside { + padding-top: 1em; + padding-bottom: 1em; + @include text-shadow(0 1px 0 lighten($aside-notice-bg, 15%)); + margin-top: 1.5em; + margin-bottom: 1.5em; + background: $aside-notice-bg; + line-height: 1.6; + + &.warning { + background-color: $aside-warning-bg; + @include text-shadow(0 1px 0 lighten($aside-warning-bg, 15%)); + } + + &.success { + background-color: $aside-success-bg; + @include text-shadow(0 1px 0 lighten($aside-success-bg, 15%)); + } + } + + aside:before { + vertical-align: middle; + padding-right: 0.5em; + font-size: 14px; + } + + aside.notice:before { + @extend %icon-info-sign; + } + + aside.warning:before { + @extend %icon-exclamation-sign; + } + + aside.success:before { + @extend %icon-ok-sign; + } + + .search-highlight { + padding: 2px; + margin: -2px; + border-radius: 4px; + border: 1px solid #F7E633; + @include text-shadow(1px 1px 0 #666); + background: linear-gradient(to top left, #F7E633 0%, #F1D32F 100%); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CODE SAMPLE STYLES +//////////////////////////////////////////////////////////////////////////////// +// This is all the stuff that appears in the right half of the page + +.content { + pre, blockquote { + background-color: $code-bg; + color: #fff; + + padding: 2em $main-padding; + margin: 0; + width: $examples-width; + + float:right; + clear:right; + + box-sizing: border-box; + @include text-shadow(0px 1px 2px rgba(0,0,0,0.4)); + + @extend %right-col; + + &>p { margin: 0; } + + a { + color: #fff; + text-decoration: none; + border-bottom: dashed 1px #ccc; + } + } + + pre { + @extend %code-font; + } + + blockquote { + &>p { + background-color: $code-annotation-bg; + border-radius: 5px; + padding: $code-annotation-padding; + color: #ccc; + border-top: 1px solid #000; + border-bottom: 1px solid #404040; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// RESPONSIVE DESIGN +//////////////////////////////////////////////////////////////////////////////// +// These are the styles for phones and tablets +// There are also a couple styles disperesed + +@media (max-width: $tablet-width) { + .tocify-wrapper { + left: -$nav-width; + + &.open { + left: 0; + } + } + + .page-wrapper { + margin-left: 0; + } + + #nav-button { + display: block; + } + + .tocify-wrapper .tocify-item > a { + padding-top: 0.3em; + padding-bottom: 0.3em; + } +} + +@media (max-width: $phone-width) { + .dark-box { + display: none; + } + + %left-col { + margin-right: 0; + } + + .tocify-wrapper .lang-selector { + display: block; + } + + .page-wrapper .lang-selector { + display: none; + } + + %right-col { + width: auto; + float: none; + } + + %right-col + %left-col { + margin-top: $main-padding; + } +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java b/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java new file mode 100644 index 000000000..b32d0a119 --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/Note.java @@ -0,0 +1,75 @@ +/* + * Copyright 2014-2016 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 com.example.notes; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +public class Note { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + private String title; + + private String body; + + @ManyToMany + private List tags; + + @JsonIgnore + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java b/samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java new file mode 100644 index 000000000..0325c49aa --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/NoteRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2014-2016 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 com.example.notes; + +import org.springframework.data.repository.CrudRepository; + +public interface NoteRepository extends CrudRepository { + +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java new file mode 100644 index 000000000..32c5ce243 --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014-2016 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 com.example.notes; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@EnableAutoConfiguration +@ComponentScan +@EnableJpaRepositories +public class RestNotesSlate { + + public static void main(String[] args) { + SpringApplication.run(RestNotesSlate.class, args); + } + +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java b/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java new file mode 100644 index 000000000..970b642f4 --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/Tag.java @@ -0,0 +1,65 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.notes; + +import java.util.List; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +public class Tag { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + private String name; + + @ManyToMany(mappedBy = "tags") + private List notes; + + @JsonIgnore + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getNotes() { + return notes; + } + + public void setNotes(List notes) { + this.notes = notes; + } +} diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java b/samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java new file mode 100644 index 000000000..8790619bb --- /dev/null +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/TagRepository.java @@ -0,0 +1,23 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.notes; + +import org.springframework.data.repository.CrudRepository; + +public interface TagRepository extends CrudRepository { + +} diff --git a/samples/rest-notes-slate/src/main/resources/application.properties b/samples/rest-notes-slate/src/main/resources/application.properties new file mode 100644 index 000000000..8e06a8284 --- /dev/null +++ b/samples/rest-notes-slate/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.jackson.serialization.indent_output: true \ No newline at end of file diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java new file mode 100644 index 000000000..d4ea4ca06 --- /dev/null +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -0,0 +1,343 @@ +/* + * Copyright 2014-2016 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 com.example.notes; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +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.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.restdocs.templates.TemplateFormats.markdown; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.RequestDispatcher; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.hateoas.MediaTypes; +import org.springframework.restdocs.JUnitRestDocumentation; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = RestNotesSlate.class) +@WebAppConfiguration +public class ApiDocumentation { + + @Rule + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + + @Autowired + private NoteRepository noteRepository; + + @Autowired + private TagRepository tagRepository; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private WebApplicationContext context; + + private MockMvc mockMvc; + + @Before + public void setUp() { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation).snippets() + .withTemplateFormat(markdown())).build(); + } + + @Test + public void errorExample() throws Exception { + this.mockMvc + .perform(get("/error") + .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) + .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, + "/notes") + .requestAttr(RequestDispatcher.ERROR_MESSAGE, + "The tag 'http://localhost:8080/tags/123' does not exist")) + .andDo(print()).andExpect(status().isBadRequest()) + .andExpect(jsonPath("error", is("Bad Request"))) + .andExpect(jsonPath("timestamp", is(notNullValue()))) + .andExpect(jsonPath("status", is(400))) + .andExpect(jsonPath("path", is(notNullValue()))) + .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")))); + } + + @Test + public void indexExample() throws Exception { + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(document("index-example", + links( + linkWithRel("notes").description("The [Notes](#notes) resource"), + linkWithRel("tags").description("The [Tags](#tags) resource"), + linkWithRel("profile").description("The ALPS profile for the service")), + responseFields( + fieldWithPath("_links").description("Links to other resources")))); + + } + + @Test + public void notesListExample() throws Exception { + this.noteRepository.deleteAll(); + + createNote("REST maturity model", + "http://martinfowler.com/articles/richardsonMaturityModel.html"); + createNote("Hypertext Application Language (HAL)", + "http://stateless.co/hal_specification.html"); + createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); + + this.mockMvc.perform(get("/notes")) + .andExpect(status().isOk()) + .andDo(document("notes-list-example", + responseFields( + fieldWithPath("_embedded.notes").description("An array of [Note](#note) resources")))); + } + + @Test + public void notesCreateExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + Map note = new HashMap(); + note.put("title", "REST maturity model"); + note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); + note.put("tags", Arrays.asList(tagLocation)); + + this.mockMvc.perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(note))).andExpect( + status().isCreated()) + .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](#tag) resource URIs")))); + } + + @Test + public void noteGetExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + Map note = new HashMap(); + note.put("title", "REST maturity model"); + note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); + note.put("tags", Arrays.asList(tagLocation)); + + String noteLocation = this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + this.mockMvc.perform(get(noteLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(note.get("title")))) + .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", + links( + linkWithRel("self").description("This note"), + 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("Links to other resources")))); + } + + @Test + public void tagsListExample() throws Exception { + this.noteRepository.deleteAll(); + this.tagRepository.deleteAll(); + + createTag("REST"); + createTag("Hypermedia"); + createTag("HTTP"); + + this.mockMvc.perform(get("/tags")) + .andExpect(status().isOk()) + .andDo(document("tags-list-example", + responseFields( + fieldWithPath("_embedded.tags").description("An array of [Tag](#tag) resources")))); + } + + @Test + public void tagsCreateExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + this.mockMvc.perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andDo(document("tags-create-example", + requestFields( + fieldWithPath("name").description("The name of the tag")))); + } + + @Test + public void noteUpdateExample() throws Exception { + Map note = new HashMap(); + note.put("title", "REST maturity model"); + note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); + + String noteLocation = this.mockMvc + .perform( + post("/notes").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) + .andExpect(jsonPath("title", is(note.get("title")))) + .andExpect(jsonPath("body", is(note.get("body")))) + .andExpect(jsonPath("_links.self.href", is(noteLocation))) + .andExpect(jsonPath("_links.tags", is(notNullValue()))); + + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + Map noteUpdate = new HashMap(); + noteUpdate.put("tags", Arrays.asList(tagLocation)); + + this.mockMvc.perform( + patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(noteUpdate))) + .andExpect(status().isNoContent()) + .andDo(document("note-update-example", + requestFields( + fieldWithPath("title").description("The title of the note").type(JsonFieldType.STRING).optional(), + fieldWithPath("body").description("The body of the note").type(JsonFieldType.STRING).optional(), + fieldWithPath("tags").description("An array of [tag](#tag) resource URIs").optional()))); + } + + @Test + public void tagGetExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + this.mockMvc.perform(get(tagLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("name", is(tag.get("name")))) + .andDo(document("tag-get-example", + links( + linkWithRel("self").description("This tag"), + linkWithRel("notes").description("The notes that have this tag")), + responseFields( + fieldWithPath("name").description("The name of the tag"), + fieldWithPath("_links").description("Links to other resources")))); + } + + @Test + public void tagUpdateExample() throws Exception { + Map tag = new HashMap(); + tag.put("name", "REST"); + + String tagLocation = this.mockMvc + .perform( + post("/tags").contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()).andReturn().getResponse() + .getHeader("Location"); + + Map tagUpdate = new HashMap(); + tagUpdate.put("name", "RESTful"); + + this.mockMvc.perform( + patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( + this.objectMapper.writeValueAsString(tagUpdate))) + .andExpect(status().isNoContent()) + .andDo(document("tag-update-example", + requestFields( + fieldWithPath("name").description("The name of the tag")))); + } + + private void createNote(String title, String body) { + Note note = new Note(); + note.setTitle(title); + note.setBody(body); + + this.noteRepository.save(note); + } + + private void createTag(String name) { + Tag tag = new Tag(); + tag.setName(name); + this.tagRepository.save(tag); + } +} From 4c5095589ac96a9e41064b1b83b27dedfd3bedc1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 15 Feb 2016 17:57:51 +0000 Subject: [PATCH 069/898] Ensure that REST Assured module is installed before sample is built --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 67a98120c..1c00729c5 100644 --- a/build.gradle +++ b/build.gradle @@ -154,6 +154,7 @@ configure(subprojects - project(":docs")) { subproject -> samples { dependOn 'spring-restdocs-core:install' dependOn 'spring-restdocs-mockmvc:install' + dependOn 'spring-restdocs-restassured:install' restNotesSpringHateoas { workingDir "$projectDir/samples/rest-notes-spring-hateoas" From d84fca4d63b55bf7caf0cf5224875ff984bc927a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Feb 2016 10:47:07 +0000 Subject: [PATCH 070/898] Ensure that version updates are applied to Slate sample --- build.gradle | 5 +++ .../build/SampleBuildConfigurer.groovy | 42 +++++++++---------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index 1c00729c5..f6f40e613 100644 --- a/build.gradle +++ b/build.gradle @@ -171,6 +171,11 @@ samples { restAssured { workingDir "$projectDir/samples/rest-assured" } + + slate { + workingDir "$projectDir/samples/rest-notes-slate" + build false + } } task api (type: Javadoc) { diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index c960ee1ea..7ce726de8 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -29,7 +29,7 @@ public class SampleBuildConfigurer { private String workingDir - private boolean verifyIncludes = true + private boolean build = true SampleBuildConfigurer(String name) { this.name = name @@ -39,31 +39,41 @@ public class SampleBuildConfigurer { this.workingDir = workingDir } + void build(boolean build) { + this.build = build + } + Task createTask(Project project, Object... dependencies) { File sampleDir = new File(this.workingDir).absoluteFile + Task sampleBuild = project.tasks.create name sampleBuild.description = "Builds the ${name} sample" sampleBuild.group = "Build" + if (new File(sampleDir, 'build.gradle').isFile()) { - Task gradleBuild = createGradleBuild(project, dependencies) - if (verifyIncludes) { + if (build) { + Task gradleBuild = createGradleBuild(project, dependencies) Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'build/asciidoc')) verifyIncludesTask.dependsOn gradleBuild sampleBuild.dependsOn verifyIncludesTask } - else { - sampleBuild.dependsOn gradleBuild + sampleBuild.doFirst { + replaceVersion(new File(this.workingDir, 'build.gradle'), + "springRestdocsVersion = '.*'", + "springRestdocsVersion = '${project.version}'") } } else if (new File(sampleDir, 'pom.xml').isFile()) { - Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) - if (verifyIncludes) { + if (build) { + Task mavenBuild = createMavenBuild(project, sampleDir, dependencies) Task verifyIncludesTask = createVerifyIncludes(project, new File(sampleDir, 'target/generated-docs')) verifyIncludesTask.dependsOn(mavenBuild) sampleBuild.dependsOn verifyIncludesTask } - else { - sampleBuild.dependsOn mavenBuild + sampleBuild.doFirst { + replaceVersion(new File(this.workingDir, 'pom.xml'), + '.*', + "${project.version}") } } else { @@ -79,13 +89,6 @@ public class SampleBuildConfigurer { mavenBuild.workingDir = this.workingDir mavenBuild.commandLine = [isWindows() ? "${sampleDir.absolutePath}/mvnw.cmd" : './mvnw', 'clean', 'package'] mavenBuild.dependsOn dependencies - - mavenBuild.doFirst { - replaceVersion(new File(this.workingDir, 'pom.xml'), - '.*', - "${project.version}") - } - return mavenBuild } @@ -100,13 +103,6 @@ public class SampleBuildConfigurer { gradleBuild.dir = this.workingDir gradleBuild.tasks = ['clean', 'build'] gradleBuild.dependsOn dependencies - - gradleBuild.doFirst { - replaceVersion(new File(this.workingDir, 'build.gradle'), - "springRestdocsVersion = '.*'", - "springRestdocsVersion = '${project.version}'") - } - return gradleBuild } From 36bacb470bc764b3bd58314147b120ebb851ee9d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 16 Feb 2016 11:11:53 +0000 Subject: [PATCH 071/898] Move to the offical SonarQube plugin --- build.gradle | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 743c45bb5..b6e05c519 100644 --- a/build.gradle +++ b/build.gradle @@ -2,11 +2,13 @@ buildscript { repositories { jcenter() maven { url 'https://repo.spring.io/plugins-release' } + maven { url 'https://plugins.gradle.org/m2/' } } dependencies { classpath 'io.spring.gradle:dependency-management-plugin:0.5.3.RELEASE' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' classpath 'io.spring.gradle:spring-io-plugin:0.0.4.RELEASE' + classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:1.2' } } @@ -18,10 +20,10 @@ allprojects { } apply plugin: 'samples' -apply plugin: 'sonar-runner' +apply plugin: 'org.sonarqube' -sonarRunner { - sonarProperties { +sonarqube { + properties { property 'sonar.branch', '1.0.x' property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" property 'sonar.java.coveragePlugin', 'jacoco' From ee865ce4ede415b980bcd90626136fa7f370bdb9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 22 Feb 2016 10:13:35 +0000 Subject: [PATCH 072/898] Tolerate relocated test-related classes in Spring Boot 1.4 --- spring-restdocs-restassured/build.gradle | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index e7c33f79c..5f495f507 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,3 +1,8 @@ +boolean isPlatformUsingBootOneFour() { + def bootVersion = dependencyManagement.springIoTestRuntime.managedVersions['org.springframework.boot:spring-boot'] + bootVersion.startsWith('1.4') +} + dependencies { compile project(':spring-restdocs-core') compile 'com.jayway.restassured:rest-assured' @@ -13,4 +18,12 @@ dependencies { test { jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" -} \ No newline at end of file +} + +afterEvaluate { + if (project.hasProperty('platformVersion') && platformUsingBootOneFour) { + dependencies { + springIoTestRuntime 'org.springframework.boot:spring-boot-test' + } + } +} From 536e49287d56258c3e144169e26abbfe11c7904d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 1 Mar 2016 09:38:43 +0000 Subject: [PATCH 073/898] Update tests to cope with different Content-Type charset attribute --- ...kMvcRestDocumentationIntegrationTests.java | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 921e3f8b0..162db0d1d 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -159,7 +159,6 @@ public void curlSnippetWithContent() throws Exception { public void curlSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha") .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) .andDo(document("curl-snippet-with-query-string")); @@ -341,14 +340,12 @@ public void preprocessedResponse() throws Exception { + "\"href\":\"href\"}]}"; assertThat( new File("build/generated-snippets/original-response/http-response.adoc"), - is(snippet(asciidoctor()) - .withContents( - httpResponse(asciidoctor(), HttpStatus.OK) - .header("a", "alpha") - .header("Content-Type", "application/json") - .header(HttpHeaders.CONTENT_LENGTH, - original.getBytes().length) - .content(original)))); + is(snippet(asciidoctor()).withContents( + httpResponse(asciidoctor(), HttpStatus.OK).header("a", "alpha") + .header("Content-Type", "application/json;charset=UTF-8") + .header(HttpHeaders.CONTENT_LENGTH, + original.getBytes().length) + .content(original)))); String prettyPrinted = String.format("{%n \"a\" : \"<>\",%n \"links\" : " + "[ {%n \"rel\" : \"rel\",%n \"href\" : \"...\"%n } ]%n}"); assertThat( @@ -356,7 +353,7 @@ public void preprocessedResponse() throws Exception { "build/generated-snippets/preprocessed-response/http-response.adoc"), is(snippet(asciidoctor()) .withContents(httpResponse(asciidoctor(), HttpStatus.OK) - .header("Content-Type", "application/json") + .header("Content-Type", "application/json;charset=UTF-8") .header(HttpHeaders.CONTENT_LENGTH, prettyPrinted.getBytes().length) .content(prettyPrinted)))); @@ -428,7 +425,7 @@ public TestController testController() { @RestController private static class TestController { - @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) + @RequestMapping(value = "/", produces = "application/json;charset=UTF-8") public ResponseEntity> foo() { Map response = new HashMap<>(); response.put("a", "alpha"); From b26d8c085ddcab7af6c62991839e787cc4d4f577 Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Tue, 1 Mar 2016 16:35:32 -0500 Subject: [PATCH 074/898] Add support for generating an HTTPie snippet This commit adds support for generating a snippet that contains the HTTPie command for the request. As the snippet does not require any additional configuration, it has added to the existing default snippets. Httpie does not currently support setting the content type for each part in a multipart form -- these multipart types are currently ignored. See: https://github.com/jkbrzt/httpie/issues/199 https://github.com/jkbrzt/httpie/issues/271 https://github.com/jkbrzt/httpie/pull/285 https://github.com/jkbrzt/httpie/pull/398 There is an issue with specifying piped input for multipart form data. There is no way currently to specify the data without specifying a filename parameter in the Content-Disposition. For now, this is ignored. See: https://github.com/jkbrzt/httpie/issues/342 See gh-207 --- config/checkstyle/checkstyle.xml | 2 +- .../docs/asciidoc/documenting-your-api.adoc | 4 + docs/src/docs/asciidoc/getting-started.adoc | 3 +- .../mockmvc/CustomDefaultSnippets.java | 3 +- .../restassured/CustomDefaultSnippets.java | 3 +- .../restdocs/cli/AbstractCliSnippet.java | 182 ++++++++++ .../{curl => cli}/QueryStringParser.java | 2 +- .../restdocs/cli/curl/CurlDocumentation.java | 60 ++++ .../restdocs/cli/curl/CurlRequestSnippet.java | 161 +++++++++ .../restdocs/{ => cli}/curl/package-info.java | 2 +- .../cli/httpie/HttpieDocumentation.java | 59 ++++ .../cli/httpie/HttpieRequestSnippet.java | 183 ++++++++++ .../restdocs/cli/httpie/package-info.java | 20 ++ .../restdocs/config/SnippetConfigurer.java | 11 +- .../restdocs/curl/CurlDocumentation.java | 9 +- .../restdocs/curl/CurlRequestSnippet.java | 230 +----------- .../default-httpie-request.snippet | 4 + .../markdown/default-httpie-request.snippet | 3 + .../{curl => cli}/QueryStringParserTests.java | 2 +- .../curl/CurlRequestSnippetTests.java | 2 +- .../cli/httpie/HttpieRequestSnippetTests.java | 330 ++++++++++++++++++ .../RestDocumentationConfigurerTests.java | 6 +- .../restdocs/test/ExpectedSnippet.java | 5 + .../httpie-request-with-title.snippet | 5 + .../httpie-request-with-title.snippet | 4 + ...kMvcRestDocumentationIntegrationTests.java | 2 +- 26 files changed, 1061 insertions(+), 236 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{curl => cli}/QueryStringParser.java (98%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{ => cli}/curl/package-info.java (93%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{curl => cli}/QueryStringParserTests.java (98%) rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{ => cli}/curl/CurlRequestSnippetTests.java (99%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 36c8db5b7..86947f995 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.cli.curl.CurlDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.templates.TemplateFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index fb9977c85..f036275c9 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -542,6 +542,10 @@ A number of snippets are produced automatically when you document a request and | Contains the http://curl.haxx.se[`curl`] command that is equivalent to the `MockMvc` call that is being documented +| `httpie-request.adoc` +| Contains the http://httpie.org[`HTTPie`] command that is equivalent to the `MockMvc` +call that is being documented + | `http-request.adoc` | Contains the HTTP request that is equivalent to the `MockMvc` call that is being documented diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 67d7dc178..2e2cc2e3f 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -371,9 +371,10 @@ static `document` method on <4> Invoke the root (`/`) of the service. <5> Assert that the service produce the expected response. -By default, three snippets are written: +By default, four snippets are written: * `/index/curl-request.adoc` + * `/index/httpie-request.adoc` * `/index/http-request.adoc` * `/index/http-response.adoc` diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 4be61c375..7164e0581 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -20,12 +20,11 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index ae581cd05..632507c6a 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -19,12 +19,11 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java new file mode 100644 index 000000000..ef00f0ba6 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java @@ -0,0 +1,182 @@ +/* + * Copyright 2014-2016 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.cli; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Base64Utils; + +/** + * An abstract {@link Snippet} that for CLI requests. + * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer + * @author Raman Gupta + */ +public abstract class AbstractCliSnippet extends TemplatedSnippet { + + private static final Set HEADER_FILTERS; + + static { + Set headerFilters = new HashSet<>(); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); + headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); + headerFilters.add(new BasicAuthHeaderFilter()); + HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); + } + + /** + * Create a new abstract cli snippet with the given name and attributes. + * @param snippetName The snippet name. + * @param attributes The snippet attributes. + */ + protected AbstractCliSnippet(String snippetName, Map attributes) { + super(snippetName, attributes); + } + + /** + * Create the model which will be passed to the template for rendering. + * @param operation The operation + * @return The model. + */ + protected abstract Map createModel(Operation operation); + + /** + * Gets the unique parameters given a request. + * @param request The operation request. + * @return The unique parameters. + */ + protected Parameters getUniqueParameters(OperationRequest request) { + Parameters queryStringParameters = new QueryStringParser() + .parse(request.getUri()); + Parameters uniqueParameters = new Parameters(); + + for (Map.Entry> parameter : request.getParameters().entrySet()) { + addIfUnique(parameter, queryStringParameters, uniqueParameters); + } + return uniqueParameters; + } + + private void addIfUnique(Map.Entry> parameter, + Parameters queryStringParameters, Parameters uniqueParameters) { + if (!queryStringParameters.containsKey(parameter.getKey())) { + uniqueParameters.put(parameter.getKey(), parameter.getValue()); + } + else { + List candidates = parameter.getValue(); + List existing = queryStringParameters.get(parameter.getKey()); + for (String candidate : candidates) { + if (!existing.contains(candidate)) { + uniqueParameters.add(parameter.getKey(), candidate); + } + } + } + } + + /** + * Whether the request operation is a PUT or a POST. + * @param request The request. + * @return boolean + */ + protected boolean isPutOrPost(OperationRequest request) { + return HttpMethod.PUT.equals(request.getMethod()) + || HttpMethod.POST.equals(request.getMethod()); + } + + /** + * Whether the passed header is allowed according to the configured + * header filters. + * @param header The header to test. + * @return boolean + */ + protected boolean allowedHeader(Map.Entry> header) { + for (HeaderFilter headerFilter : HEADER_FILTERS) { + if (!headerFilter.allow(header.getKey(), header.getValue())) { + return false; + } + } + return true; + } + + /** + * Determine if the header passed is a basic auth header. + * @param headerValue The header to test. + * @return boolean + */ + protected boolean isBasicAuthHeader(List headerValue) { + return BasicAuthHeaderFilter.isBasicAuthHeader(headerValue); + } + + /** + * Decodes a basic auth header into name:password credentials. + * @param headerValue The encoded header value. + * @return name:password credentials. + */ + protected String decodeBasicAuthHeader(List headerValue) { + return BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); + } + + private interface HeaderFilter { + + boolean allow(String name, List value); + } + + private static final class BasicAuthHeaderFilter implements HeaderFilter { + + @Override + public boolean allow(String name, List value) { + return !(HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)); + } + + static boolean isBasicAuthHeader(List value) { + return value != null && (!value.isEmpty()) + && value.get(0).startsWith("Basic "); + } + + static String decodeBasicAuthHeader(List value) { + return new String(Base64Utils.decodeFromString(value.get(0).substring(6))); + } + + } + + private static final class NamedHeaderFilter implements HeaderFilter { + + private final String name; + + NamedHeaderFilter(String name) { + this.name = name; + } + + @Override + public boolean allow(String name, List value) { + return !this.name.equalsIgnoreCase(name); + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java similarity index 98% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java index ea5f31481..4a3ab8ca1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli; import java.io.UnsupportedEncodingException; import java.net.URI; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java new file mode 100644 index 000000000..73ef5fe4d --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java @@ -0,0 +1,60 @@ +/* + * 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.cli.curl; + +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API as if it were being driven using + * the cURL command-line utility. + * + * @author Andy Wilkinson + * @author Yann Le Guern + * @author Dmitriy Mayboroda + * @author Jonathan Pearlin + */ +public abstract class CurlDocumentation { + + private CurlDocumentation() { + + } + + /** + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. + * + * @return the snippet that will document the curl request + */ + public static Snippet curlRequest() { + return new CurlRequestSnippet(); + } + + /** + * Returns a new {@code Snippet} that will document the curl request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the curl request + */ + public static Snippet curlRequest(Map attributes) { + return new CurlRequestSnippet(attributes); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java new file mode 100644 index 000000000..af4c253c8 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java @@ -0,0 +1,161 @@ +/* + * Copyright 2014-2016 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.cli.curl; + +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.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.restdocs.cli.AbstractCliSnippet; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.StringUtils; + +/** + * A {@link Snippet} that documents the curl command for a request. + * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer + * @see CurlDocumentation#curlRequest() + * @see CurlDocumentation#curlRequest(Map) + */ +public class CurlRequestSnippet extends AbstractCliSnippet { + + /** + * Creates a new {@code CurlRequestSnippet} with no additional attributes. + */ + protected CurlRequestSnippet() { + this(null); + } + + /** + * Creates a new {@code CurlRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected CurlRequestSnippet(Map attributes) { + super("curl-request", attributes); + } + + @Override + protected Map createModel(Operation operation) { + Map model = new HashMap<>(); + model.put("url", getUrl(operation)); + model.put("options", getOptions(operation)); + return model; + } + + private String getUrl(Operation operation) { + return String.format("'%s'", operation.getRequest().getUri()); + } + + private String getOptions(Operation operation) { + StringWriter command = new StringWriter(); + PrintWriter printer = new PrintWriter(command); + writeIncludeHeadersInOutputOption(printer); + writeUserOptionIfNecessary(operation.getRequest(), printer); + writeHttpMethodIfNecessary(operation.getRequest(), printer); + writeHeaders(operation.getRequest().getHeaders(), printer); + writePartsIfNecessary(operation.getRequest(), printer); + writeContent(operation.getRequest(), printer); + + return command.toString(); + } + + private void writeIncludeHeadersInOutputOption(PrintWriter writer) { + writer.print("-i"); + } + + private void writeUserOptionIfNecessary(OperationRequest request, + PrintWriter writer) { + List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (isBasicAuthHeader(headerValue)) { + String credentials = decodeBasicAuthHeader(headerValue); + writer.print(String.format(" -u '%s'", credentials)); + } + } + + private void writeHttpMethodIfNecessary(OperationRequest request, + PrintWriter writer) { + if (!HttpMethod.GET.equals(request.getMethod())) { + writer.print(String.format(" -X %s", request.getMethod())); + } + } + + private void writeHeaders(HttpHeaders headers, PrintWriter writer) { + for (Entry> entry : headers.entrySet()) { + if (allowedHeader(entry)) { + for (String header : entry.getValue()) { + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); + } + } + } + } + + private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { + for (OperationRequestPart part : request.getParts()) { + writer.printf(" -F '%s=", part.getName()); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + writer.append(part.getContentAsString()); + } + else { + writer.printf("@%s", part.getSubmittedFileName()); + } + if (part.getHeaders().getContentType() != null) { + writer.append(";type=") + .append(part.getHeaders().getContentType().toString()); + } + + writer.append("'"); + } + } + + private void writeContent(OperationRequest request, PrintWriter writer) { + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + writer.print(String.format(" -d '%s'", content)); + } + else if (!request.getParts().isEmpty()) { + for (Entry> entry : request.getParameters().entrySet()) { + for (String value : entry.getValue()) { + writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); + } + } + } + else if (isPutOrPost(request)) { + writeContentUsingParameters(request, writer); + } + } + + private void writeContentUsingParameters(OperationRequest request, + PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + String queryString = uniqueParameters.toQueryString(); + if (StringUtils.hasText(queryString)) { + writer.print(String.format(" -d '%s'", queryString)); + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java similarity index 93% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java index 7c07b91e0..02ebe62cd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java @@ -17,4 +17,4 @@ /** * Documenting the curl command required to make a request to a RESTful API. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli.curl; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java new file mode 100644 index 000000000..0313ae027 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java @@ -0,0 +1,59 @@ +/* + * 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.cli.httpie; + +import java.util.Map; + +import org.springframework.restdocs.snippet.Snippet; + +/** + * Static factory methods for documenting a RESTful API as if it were being driven using + * the httpie command-line utility. + * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer + * @author Raman Gupta + */ +public abstract class HttpieDocumentation { + + private HttpieDocumentation() { + + } + + /** + * Returns a new {@code Snippet} that will document the httpie request for the API + * operation. + * + * @return the snippet that will document the httpie request + */ + public static Snippet httpieRequest() { + return new HttpieRequestSnippet(); + } + + /** + * Returns a new {@code Snippet} that will document the httpie request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the httpie request + */ + public static Snippet httpieRequest(Map attributes) { + return new HttpieRequestSnippet(attributes); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java new file mode 100644 index 000000000..7e251a90e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java @@ -0,0 +1,183 @@ +/* + * Copyright 2014-2016 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.cli.httpie; + +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.http.HttpHeaders; +import org.springframework.restdocs.cli.AbstractCliSnippet; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.util.StringUtils; + +/** + * A {@link Snippet} that documents the httpie command for a request. + * + * @author Raman Gupta + * @see HttpieDocumentation#httpieRequest() + * @see HttpieDocumentation#httpieRequest(Map) + */ +public class HttpieRequestSnippet extends AbstractCliSnippet { + + /** + * Creates a new {@code CurlRequestSnippet} with no additional attributes. + */ + protected HttpieRequestSnippet() { + this(null); + } + + /** + * Creates a new {@code CurlRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected HttpieRequestSnippet(Map attributes) { + super("httpie-request", attributes); + } + + @Override + protected Map createModel(final Operation operation) { + Map model = new HashMap<>(); + model.put("echo_content", getContentStdIn(operation)); + model.put("options", getOptions(operation)); + model.put("url", getUrl(operation)); + model.put("request_items", getRequestItems(operation)); + return model; + } + private Object getContentStdIn(final Operation operation) { + OperationRequest request = operation.getRequest(); + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + return String.format("echo '%s' | ", content); + } + else { + return ""; + } + } + + private String getOptions(Operation operation) { + StringWriter options = new StringWriter(); + PrintWriter printer = new PrintWriter(options); + writeOptions(operation.getRequest(), printer); + writeUserOptionIfNecessary(operation.getRequest(), printer); + writeMethodIfNecessary(operation.getRequest(), printer); + return options.toString(); + } + + private String getUrl(Operation operation) { + return String.format("'%s'", operation.getRequest().getUri()); + } + + private String getRequestItems(final Operation operation) { + StringWriter requestItems = new StringWriter(); + PrintWriter printer = new PrintWriter(requestItems); + writeFormDataIfNecessary(operation.getRequest(), printer); + writeHeaders(operation.getRequest(), printer); + writeContent(operation.getRequest(), printer); + return requestItems.toString(); + } + private void writeOptions(OperationRequest request, PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + if (request.getParts().size() > 0 || uniqueParameters.size() > 0) { + writer.print("--form "); + } + } + + private void writeUserOptionIfNecessary(OperationRequest request, + PrintWriter writer) { + List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + if (isBasicAuthHeader(headerValue)) { + String credentials = decodeBasicAuthHeader(headerValue); + writer.print(String.format("--auth '%s' ", credentials)); + } + } + + private void writeMethodIfNecessary(OperationRequest request, PrintWriter writer) { + writer.print(String.format("%s", request.getMethod().name())); + } + + private void writeFormDataIfNecessary(OperationRequest request, PrintWriter writer) { + for (OperationRequestPart part : request.getParts()) { + writer.printf(" \\\n '%s'", part.getName()); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + // httpie https://github.com/jkbrzt/httpie/issues/342 + writer.printf("@<(echo '%s')", part.getContentAsString()); + } + else { + writer.printf("@'%s'", part.getSubmittedFileName()); + } + // httpie does not currently support manually set content type by part + } + } + + private void writeHeaders(OperationRequest request, PrintWriter writer) { + HttpHeaders headers = request.getHeaders(); + for (Entry> entry : headers.entrySet()) { + if (allowedHeader(entry)) { + for (String header : entry.getValue()) { + // form Content-Type not required, added automatically by httpie with --form + if (request.getParts().size() > 0 + && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) + && header.startsWith("multipart/form-data")) { + continue; + } + writer.print(String.format(" '%s:%s'", entry.getKey(), header)); + } + } + } + } + + private void writeContent(OperationRequest request, PrintWriter writer) { + String content = request.getContentAsString(); + if (!StringUtils.hasText(content)) { + if (!request.getParts().isEmpty()) { + for (Entry> entry : request.getParameters().entrySet()) { + for (String value : entry.getValue()) { + writer.print(String.format(" '%s=%s'", entry.getKey(), value)); + } + } + } + else if (isPutOrPost(request)) { + writeContentUsingParameters(request, writer); + } + } + } + + private void writeContentUsingParameters(OperationRequest request, + PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + for (Map.Entry> entry : uniqueParameters.entrySet()) { + if (entry.getValue().isEmpty()) { + writer.append(String.format(" '%s='", entry.getKey())); + } + else { + for (String value : entry.getValue()) { + writer.append(String.format(" '%s=%s'", entry.getKey(), value)); + } + } + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java new file mode 100644 index 000000000..2bdf5df66 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Documenting the httpie command required to make a request to a RESTful API. + */ +package org.springframework.restdocs.cli.httpie; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index e8faa6ecf..7c2b459c7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -21,7 +21,8 @@ import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.curl.CurlDocumentation; +import org.springframework.restdocs.cli.curl.CurlDocumentation; +import org.springframework.restdocs.cli.httpie.HttpieDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; @@ -38,8 +39,12 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer { - private List defaultSnippets = Arrays.asList(CurlDocumentation.curlRequest(), - HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse()); + private List defaultSnippets = Arrays.asList( + CurlDocumentation.curlRequest(), + HttpieDocumentation.httpieRequest(), + HttpDocumentation.httpRequest(), + HttpDocumentation.httpResponse() + ); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index a3549443b..a9b094f6c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -24,11 +24,14 @@ * Static factory methods for documenting a RESTful API as if it were being driven using * the cURL command-line utility. * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. + * * @author Andy Wilkinson * @author Yann Le Guern * @author Dmitriy Mayboroda * @author Jonathan Pearlin */ +@Deprecated public abstract class CurlDocumentation { private CurlDocumentation() { @@ -40,6 +43,8 @@ private CurlDocumentation() { * operation. * * @return the snippet that will document the curl request + * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. */ public static Snippet curlRequest() { return new CurlRequestSnippet(); @@ -52,6 +57,8 @@ public static Snippet curlRequest() { * * @param attributes the attributes * @return the snippet that will document the curl request + * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. */ public static Snippet curlRequest(Map attributes) { return new CurlRequestSnippet(attributes); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 4f4ea07b7..29396bac6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -16,243 +16,35 @@ package org.springframework.restdocs.curl; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.util.Base64Utils; -import org.springframework.util.StringUtils; /** * A {@link Snippet} that documents the curl command for a request. * - * @author Andy Wilkinson - * @author Paul-Christian Volkmer - * @see CurlDocumentation#curlRequest() - * @see CurlDocumentation#curlRequest(Map) + * @author Raman Gupta + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. */ -public class CurlRequestSnippet extends TemplatedSnippet { - - private static final Set HEADER_FILTERS; - - static { - Set headerFilters = new HashSet<>(); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); - headerFilters.add(new BasicAuthHeaderFilter()); - HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); - } +public class CurlRequestSnippet extends org.springframework.restdocs.cli.curl.CurlRequestSnippet { /** * Creates a new {@code CurlRequestSnippet} with no additional attributes. + * + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. */ protected CurlRequestSnippet() { - this(null); + super(); } /** - * Creates a new {@code CurlRequestSnippet} with the given additional - * {@code attributes} that will be included in the model during template rendering. + * Creates a new {@code CurlRequestSnippet} with additional attributes. + * @param attributes The additional attributes. * - * @param attributes The additional attributes + * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. */ - protected CurlRequestSnippet(Map attributes) { - super("curl-request", attributes); - } - - @Override - protected Map createModel(Operation operation) { - Map model = new HashMap<>(); - model.put("url", getUrl(operation)); - model.put("options", getOptions(operation)); - return model; - } - - private String getUrl(Operation operation) { - return String.format("'%s'", operation.getRequest().getUri()); - } - - private String getOptions(Operation operation) { - StringWriter command = new StringWriter(); - PrintWriter printer = new PrintWriter(command); - writeIncludeHeadersInOutputOption(printer); - writeUserOptionIfNecessary(operation.getRequest(), printer); - writeHttpMethodIfNecessary(operation.getRequest(), printer); - writeHeaders(operation.getRequest().getHeaders(), printer); - writePartsIfNecessary(operation.getRequest(), printer); - writeContent(operation.getRequest(), printer); - - return command.toString(); - } - - private void writeIncludeHeadersInOutputOption(PrintWriter writer) { - writer.print("-i"); - } - - private void writeUserOptionIfNecessary(OperationRequest request, - PrintWriter writer) { - List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); - if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { - String credentials = BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); - writer.print(String.format(" -u '%s'", credentials)); - } - } - - private void writeHttpMethodIfNecessary(OperationRequest request, - PrintWriter writer) { - if (!HttpMethod.GET.equals(request.getMethod())) { - writer.print(String.format(" -X %s", request.getMethod())); - } - } - - private void writeHeaders(HttpHeaders headers, PrintWriter writer) { - for (Entry> entry : headers.entrySet()) { - if (allowedHeader(entry)) { - for (String header : entry.getValue()) { - writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); - } - } - } - } - - private boolean allowedHeader(Entry> header) { - for (HeaderFilter headerFilter : HEADER_FILTERS) { - if (!headerFilter.allow(header.getKey(), header.getValue())) { - return false; - } - } - return true; - } - - private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) { - for (OperationRequestPart part : request.getParts()) { - writer.printf(" -F '%s=", part.getName()); - if (!StringUtils.hasText(part.getSubmittedFileName())) { - writer.append(part.getContentAsString()); - } - else { - writer.printf("@%s", part.getSubmittedFileName()); - } - if (part.getHeaders().getContentType() != null) { - writer.append(";type=") - .append(part.getHeaders().getContentType().toString()); - } - - writer.append("'"); - } - } - - private void writeContent(OperationRequest request, PrintWriter writer) { - String content = request.getContentAsString(); - if (StringUtils.hasText(content)) { - writer.print(String.format(" -d '%s'", content)); - } - else if (!request.getParts().isEmpty()) { - for (Entry> entry : request.getParameters().entrySet()) { - for (String value : entry.getValue()) { - writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); - } - } - } - else if (isPutOrPost(request)) { - writeContentUsingParameters(request, writer); - } - } - - private void writeContentUsingParameters(OperationRequest request, - PrintWriter writer) { - Parameters uniqueParameters = getUniqueParameters(request); - String queryString = uniqueParameters.toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.print(String.format(" -d '%s'", queryString)); - } - } - - private Parameters getUniqueParameters(OperationRequest request) { - Parameters queryStringParameters = new QueryStringParser() - .parse(request.getUri()); - Parameters uniqueParameters = new Parameters(); - - for (Entry> parameter : request.getParameters().entrySet()) { - addIfUnique(parameter, queryStringParameters, uniqueParameters); - } - return uniqueParameters; - } - - private void addIfUnique(Entry> parameter, - Parameters queryStringParameters, Parameters uniqueParameters) { - if (!queryStringParameters.containsKey(parameter.getKey())) { - uniqueParameters.put(parameter.getKey(), parameter.getValue()); - } - else { - List candidates = parameter.getValue(); - List existing = queryStringParameters.get(parameter.getKey()); - for (String candidate : candidates) { - if (!existing.contains(candidate)) { - uniqueParameters.add(parameter.getKey(), candidate); - } - } - } - } - - private boolean isPutOrPost(OperationRequest request) { - return HttpMethod.PUT.equals(request.getMethod()) - || HttpMethod.POST.equals(request.getMethod()); - } - - private interface HeaderFilter { - - boolean allow(String name, List value); - } - - private static final class BasicAuthHeaderFilter implements HeaderFilter { - - @Override - public boolean allow(String name, List value) { - if (HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)) { - return false; - } - return true; - } - - static boolean isBasicAuthHeader(List value) { - return value != null && (!value.isEmpty()) - && value.get(0).startsWith("Basic "); - } - - static String decodeBasicAuthHeader(List value) { - return new String(Base64Utils.decodeFromString(value.get(0).substring(6))); - } - - } - - private static final class NamedHeaderFilter implements HeaderFilter { - - private final String name; - - private NamedHeaderFilter(String name) { - this.name = name; - } - - @Override - public boolean allow(String name, List value) { - return !this.name.equalsIgnoreCase(name); - } - + protected CurlRequestSnippet(final Map attributes) { + super(attributes); } } diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet new file mode 100644 index 000000000..05771238f --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet @@ -0,0 +1,4 @@ +[source,bash] +---- +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet new file mode 100644 index 000000000..acddb53f0 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet @@ -0,0 +1,3 @@ +```bash +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java similarity index 98% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java index 634fede11..6a953e16a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli; import java.net.URI; import java.util.Arrays; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java similarity index 99% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java index 2b1371924..0d8bb6324 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.curl; +package org.springframework.restdocs.cli.curl; import java.io.IOException; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java new file mode 100644 index 000000000..1fb2abd7a --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java @@ -0,0 +1,330 @@ +/* + * Copyright 2014-2016 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.cli.httpie; + +import java.io.IOException; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.util.Base64Utils; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link HttpieRequestSnippet}. + * + * @author Andy Wilkinson + * @author Yann Le Guern + * @author Dmitriy Mayboroda + * @author Jonathan Pearlin + * @author Paul-Christian Volkmer + * @author Raman Gupta + */ +@RunWith(Parameterized.class) +public class HttpieRequestSnippetTests extends AbstractSnippetTests { + + public HttpieRequestSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void getRequest() throws IOException { + this.snippet.expectHttpieRequest("get-request").withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo'")); + new HttpieRequestSnippet().document( + operationBuilder("get-request").request("http://localhost/foo").build()); + } + + @Test + public void nonGetRequest() throws IOException { + this.snippet.expectHttpieRequest("non-get-request").withContents( + codeBlock("bash").content("$ http POST 'http://localhost/foo'")); + new HttpieRequestSnippet().document(operationBuilder("non-get-request") + .request("http://localhost/foo").method("POST").build()); + } + + @Test + public void requestWithContent() throws IOException { + this.snippet.expectHttpieRequest("request-with-content") + .withContents(codeBlock("bash") + .content("$ echo 'content' | http GET 'http://localhost/foo'")); + new HttpieRequestSnippet().document(operationBuilder("request-with-content") + .request("http://localhost/foo").content("content").build()); + } + + @Test + public void getRequestWithQueryString() throws IOException { + this.snippet.expectHttpieRequest("request-with-query-string") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document(operationBuilder("request-with-query-string") + .request("http://localhost/foo?param=value").build()); + } + + @Test + public void getRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectHttpieRequest("request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param'")); + new HttpieRequestSnippet() + .document(operationBuilder("request-with-query-string-with-no-value") + .request("http://localhost/foo?param").build()); + } + + @Test + public void postRequestWithQueryString() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-query-string") + .withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-query-string") + .request("http://localhost/foo?param=value").method("POST") + .build()); + } + + @Test + public void postRequestWithQueryStringWithNoValue() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-query-string-with-no-value") + .withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?param'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-query-string-with-no-value") + .request("http://localhost/foo?param").method("POST").build()); + } + + @Test + public void postRequestWithOneParameter() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-one-parameter") + .request("http://localhost/foo").method("POST").param("k1", "v1") + .build()); + } + + @Test + public void postRequestWithOneParameterWithNoValue() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-one-parameter-with-no-value") + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1='")); + new HttpieRequestSnippet().document( + operationBuilder("post-request-with-one-parameter-with-no-value") + .request("http://localhost/foo").method("POST").param("k1") + .build()); + } + + @Test + public void postRequestWithMultipleParameters() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-multiple-parameters") + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo'" + + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-multiple-parameters") + .request("http://localhost/foo").method("POST") + .param("k1", "v1", "v1-bis").param("k2", "v2").build()); + } + + @Test + public void postRequestWithUrlEncodedParameter() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-url-encoded-parameter") + .withContents(codeBlock("bash").content( + "$ http --form POST 'http://localhost/foo' 'k1=a&b'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-url-encoded-parameter") + .request("http://localhost/foo").method("POST").param("k1", "a&b") + .build()); + } + + @Test + public void postRequestWithQueryStringAndParameter() throws IOException { + this.snippet.expectHttpieRequest("post-request-with-query-string-and-parameter") + .withContents(codeBlock("bash").content( + "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-request-with-query-string-and-parameter") + .request("http://localhost/foo?a=alpha").method("POST") + .param("b", "bravo").build()); + } + + @Test + public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectHttpieRequest( + "post-request-with-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash").content( + "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet().document(operationBuilder( + "post-request-with-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").method("POST") + .param("a", "alpha").param("b", "bravo").build()); + } + + @Test + public void putRequestWithOneParameter() throws IOException { + this.snippet.expectHttpieRequest("put-request-with-one-parameter") + .withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); + new HttpieRequestSnippet() + .document(operationBuilder("put-request-with-one-parameter") + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .build()); + } + + @Test + public void putRequestWithMultipleParameters() throws IOException { + this.snippet.expectHttpieRequest("put-request-with-multiple-parameters") + .withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo'" + + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); + new HttpieRequestSnippet() + .document(operationBuilder("put-request-with-multiple-parameters") + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); + } + + @Test + public void putRequestWithUrlEncodedParameter() throws IOException { + this.snippet.expectHttpieRequest("put-request-with-url-encoded-parameter") + .withContents(codeBlock("bash").content( + "$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); + new HttpieRequestSnippet() + .document(operationBuilder("put-request-with-url-encoded-parameter") + .request("http://localhost/foo").method("PUT").param("k1", "a&b") + .build()); + } + + @Test + public void requestWithHeaders() throws IOException { + this.snippet.expectHttpieRequest("request-with-headers") + .withContents(codeBlock("bash").content("$ http GET 'http://localhost/foo'" + + " 'Content-Type:application/json' 'a:alpha'")); + new HttpieRequestSnippet().document( + operationBuilder("request-with-headers").request("http://localhost/foo") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); + } + + @Test + public void multipartPostWithNoSubmittedFileName() throws IOException { + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'metadata'@<(echo '{\"description\": \"foo\"}')"; + this.snippet.expectHttpieRequest("multipart-post-no-original-filename") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post-no-original-filename") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); + } + + @Test + public void multipartPostWithContentType() throws IOException { + // httpie does not yet support manually set content type by part + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'image'@'documents/images/example.png'"; + this.snippet.expectHttpieRequest("multipart-post-with-content-type") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post-with-content-type") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); + } + + @Test + public void multipartPost() throws IOException { + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'image'@'documents/images/example.png'"; + this.snippet.expectHttpieRequest("multipart-post") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); + } + + @Test + public void multipartPostWithParameters() throws IOException { + String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" + + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado' 'b=banana'"; + this.snippet.expectHttpieRequest("multipart-post-with-parameters") + .withContents(codeBlock("bash").content(expectedContent)); + new HttpieRequestSnippet() + .document(operationBuilder("multipart-post-with-parameters") + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); + } + + @Test + public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { + this.snippet.expectHttpieRequest("basic-auth").withContents(codeBlock("bash") + .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); + new HttpieRequestSnippet() + .document(operationBuilder("basic-auth").request("http://localhost/foo") + .header(HttpHeaders.AUTHORIZATION, + "Basic " + Base64Utils + .encodeToString("user:secret".getBytes())) + .build()); + } + + @Test + public void customAttributes() throws IOException { + this.snippet.expectHttpieRequest("custom-attributes") + .withContents(containsString("httpie request title")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("httpie-request")) + .willReturn(snippetResource("httpie-request-with-title")); + new HttpieRequestSnippet( + attributes( + key("title").value("httpie request title"))) + .document( + operationBuilder("custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost/foo").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0462fb306..7a36e3200 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -24,8 +24,9 @@ import org.junit.Test; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.curl.CurlDocumentation; -import org.springframework.restdocs.curl.CurlRequestSnippet; +import org.springframework.restdocs.cli.curl.CurlDocumentation; +import org.springframework.restdocs.cli.curl.CurlRequestSnippet; +import org.springframework.restdocs.cli.httpie.HttpieRequestSnippet; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; @@ -71,6 +72,7 @@ public void defaultConfiguration() { .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class), + instanceOf(HttpieRequestSnippet.class), instanceOf(HttpRequestSnippet.class), instanceOf(HttpResponseSnippet.class))); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index d706ddf6e..2799f1cc0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -76,6 +76,11 @@ public ExpectedSnippet expectCurlRequest(String name) { return this; } + public ExpectedSnippet expectHttpieRequest(String name) { + expect(name, "httpie-request"); + return this; + } + public ExpectedSnippet expectRequestFields(String name) { expect(name, "request-fields"); return this; diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet new file mode 100644 index 000000000..0bff58b19 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet @@ -0,0 +1,5 @@ +[source,bash] +.{{title}} +---- +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet new file mode 100644 index 000000000..29beade11 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet @@ -0,0 +1,4 @@ +{{title}} +```bash +$ {{echo_content}}http {{options}} {{url}}{{request_items}} +``` \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 162db0d1d..ad06b187b 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -56,7 +56,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.springframework.restdocs.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; From 37e16bc308b08d465b0a3aee3adbeeaa70877b86 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Mar 2016 17:23:04 +0000 Subject: [PATCH 075/898] Polish contribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit polishes the HTTPie request snippet contribution made in b26d8c0. It makes the following significant changes: - Applies project’s coding conventions for formatting and the like - Moves to a composition-based approach for sharing functionality between the curl and HTTPie snippets by replacing AbstractCliSnippet with CliOperationRequest. - Introduces a single package for CLI command snippets, thereby allowing more code to be package-private. See gh-207 --- config/checkstyle/checkstyle.xml | 2 +- docs/src/docs/asciidoc/configuration.adoc | 3 +- docs/src/docs/asciidoc/getting-started.adoc | 2 +- .../mockmvc/CustomDefaultSnippets.java | 10 +- .../restassured/CustomDefaultSnippets.java | 15 +- ...cumentation.java => CliDocumentation.java} | 39 +++- ...iSnippet.java => CliOperationRequest.java} | 128 ++++++------ .../cli/{curl => }/CurlRequestSnippet.java | 48 +++-- .../restdocs/cli/HttpieRequestSnippet.java | 175 +++++++++++++++++ .../cli/httpie/HttpieDocumentation.java | 59 ------ .../cli/httpie/HttpieRequestSnippet.java | 183 ------------------ .../cli/{httpie => }/package-info.java | 6 +- .../restdocs/config/SnippetConfigurer.java | 12 +- .../restdocs/curl/CurlDocumentation.java | 12 +- .../restdocs/curl/CurlRequestSnippet.java | 17 +- .../restdocs/{cli => }/curl/package-info.java | 5 +- .../default-httpie-request.snippet | 2 +- .../markdown/default-httpie-request.snippet | 2 +- .../{curl => }/CurlRequestSnippetTests.java | 2 +- .../HttpieRequestSnippetTests.java | 16 +- .../RestDocumentationConfigurerTests.java | 8 +- .../httpie-request-with-title.snippet | 2 +- .../httpie-request-with-title.snippet | 2 +- ...kMvcRestDocumentationIntegrationTests.java | 34 +++- 24 files changed, 396 insertions(+), 388 deletions(-) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/{curl/CurlDocumentation.java => CliDocumentation.java} (59%) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/{AbstractCliSnippet.java => CliOperationRequest.java} (59%) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/{curl => }/CurlRequestSnippet.java (74%) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java delete mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java rename spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/{httpie => }/package-info.java (75%) rename spring-restdocs-core/src/main/java/org/springframework/restdocs/{cli => }/curl/package-info.java (83%) rename spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/{curl => }/CurlRequestSnippetTests.java (99%) rename spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/{httpie => }/HttpieRequestSnippetTests.java (96%) diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index 86947f995..061bb5018 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -73,7 +73,7 @@ + value="com.jayway.restassured.RestAssured.*, org.junit.Assert.*, org.junit.Assume.*, org.hamcrest.CoreMatchers.*, org.hamcrest.Matchers.*, org.mockito.Mockito.*, org.mockito.BDDMockito.*, org.mockito.Matchers.*, org.springframework.restdocs.cli.CliDocumentation.*, org.springframework.restdocs.headers.HeaderDocumentation.*, org.springframework.restdocs.hypermedia.HypermediaDocumentation.*, org.springframework.restdocs.mockmvc.IterableEnumeration.*, org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*, org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*, org.springframework.restdocs.payload.PayloadDocumentation.*, org.springframework.restdocs.operation.preprocess.Preprocessors.*, org.springframework.restdocs.request.RequestDocumentation.*, org.springframework.restdocs.restassured.RestAssuredRestDocumentation.*, org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.*, org.springframework.restdocs.snippet.Attributes.*, org.springframework.restdocs.templates.TemplateFormats.*, org.springframework.restdocs.test.SnippetMatchers.*, org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*, org.springframework.test.web.servlet.result.MockMvcResultMatchers.*" /> diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index a290c72ce..00c31ac61 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -87,11 +87,12 @@ include::{examples-dir}/com/example/restassured/CustomFormat.java[tags=custom-fo [[configuration-default-snippets]] === Default snippets -Three snippets are produced by default: +Four snippets are produced by default: - `curl-request` - `http-request` - `http-response` +- `httpie-request` You can change the default snippet configuration during setup using the `RestDocumentationConfigurer` API. For example, to only produce the `curl-request` diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 2e2cc2e3f..daed3a4f1 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -374,9 +374,9 @@ static `document` method on By default, four snippets are written: * `/index/curl-request.adoc` - * `/index/httpie-request.adoc` * `/index/http-request.adoc` * `/index/http-response.adoc` + * `/index/httpie-request.adoc` Refer to <> for more information about these and other snippets that can be produced by Spring REST Docs. diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 7164e0581..4fb2990f8 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -18,19 +18,21 @@ import org.junit.Before; import org.junit.Rule; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( + "build"); @Autowired private WebApplicationContext context; @@ -41,8 +43,8 @@ public class CustomDefaultSnippets { public void setUp() { // tag::custom-default-snippets[] this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation) - .snippets().withDefaults(curlRequest())) + .apply(documentationConfiguration(this.restDocumentation).snippets() + .withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] } diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 632507c6a..57c65d360 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -16,20 +16,21 @@ package com.example.restassured; +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.specification.RequestSpecification; import org.junit.Before; import org.junit.Rule; -import org.springframework.restdocs.JUnitRestDocumentation; -import com.jayway.restassured.builder.RequestSpecBuilder; -import com.jayway.restassured.specification.RequestSpecification; +import org.springframework.restdocs.JUnitRestDocumentation; -import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; public class CustomDefaultSnippets { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( + "build"); private RequestSpecification spec; @@ -37,8 +38,8 @@ public class CustomDefaultSnippets { public void setUp() { // tag::custom-default-snippets[] this.spec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(this.restDocumentation) - .snippets().withDefaults(curlRequest())) + .addFilter(documentationConfiguration(this.restDocumentation).snippets() + .withDefaults(curlRequest())) .build(); // end::custom-default-snippets[] } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java similarity index 59% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java index 73ef5fe4d..9b6b7675f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -14,24 +14,23 @@ * limitations under the License. */ -package org.springframework.restdocs.cli.curl; +package org.springframework.restdocs.cli; import java.util.Map; import org.springframework.restdocs.snippet.Snippet; /** - * Static factory methods for documenting a RESTful API as if it were being driven using - * the cURL command-line utility. + * Static factory methods for documenting a RESTful API as if it were being driven using a + * command-line utility such as curl or HTTPie. * * @author Andy Wilkinson - * @author Yann Le Guern - * @author Dmitriy Mayboroda - * @author Jonathan Pearlin + * @author Paul-Christian Volkmer + * @author Raman Gupta */ -public abstract class CurlDocumentation { +public abstract class CliDocumentation { - private CurlDocumentation() { + private CliDocumentation() { } @@ -57,4 +56,26 @@ public static Snippet curlRequest(Map attributes) { return new CurlRequestSnippet(attributes); } + /** + * Returns a new {@code Snippet} that will document the HTTPie request for the API + * operation. + * + * @return the snippet that will document the HTTPie request + */ + public static Snippet httpieRequest() { + return new HttpieRequestSnippet(); + } + + /** + * Returns a new {@code Snippet} that will document the HTTPie request for the API + * operation. The given {@code attributes} will be available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the HTTPie request + */ + public static Snippet httpieRequest(Map attributes) { + return new HttpieRequestSnippet(attributes); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java similarity index 59% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index ef00f0ba6..99ce6aeba 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/AbstractCliSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2012-2016 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. @@ -16,29 +16,30 @@ package org.springframework.restdocs.cli; +import java.net.URI; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.snippet.Snippet; -import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.Base64Utils; /** - * An abstract {@link Snippet} that for CLI requests. + * An {@link OperationRequest} wrapper with methods that are useful when producing a + * snippet containing a CLI command for a request. * * @author Andy Wilkinson - * @author Paul-Christian Volkmer * @author Raman Gupta */ -public abstract class AbstractCliSnippet extends TemplatedSnippet { +final class CliOperationRequest implements OperationRequest { private static final Set HEADER_FILTERS; @@ -50,33 +51,19 @@ public abstract class AbstractCliSnippet extends TemplatedSnippet { HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); } - /** - * Create a new abstract cli snippet with the given name and attributes. - * @param snippetName The snippet name. - * @param attributes The snippet attributes. - */ - protected AbstractCliSnippet(String snippetName, Map attributes) { - super(snippetName, attributes); - } - - /** - * Create the model which will be passed to the template for rendering. - * @param operation The operation - * @return The model. - */ - protected abstract Map createModel(Operation operation); - - /** - * Gets the unique parameters given a request. - * @param request The operation request. - * @return The unique parameters. - */ - protected Parameters getUniqueParameters(OperationRequest request) { + private final OperationRequest delegate; + + CliOperationRequest(OperationRequest delegate) { + this.delegate = delegate; + } + + Parameters getUniqueParameters() { Parameters queryStringParameters = new QueryStringParser() - .parse(request.getUri()); + .parse(this.delegate.getUri()); Parameters uniqueParameters = new Parameters(); - for (Map.Entry> parameter : request.getParameters().entrySet()) { + for (Map.Entry> parameter : this.delegate.getParameters() + .entrySet()) { addIfUnique(parameter, queryStringParameters, uniqueParameters); } return uniqueParameters; @@ -98,23 +85,42 @@ private void addIfUnique(Map.Entry> parameter, } } - /** - * Whether the request operation is a PUT or a POST. - * @param request The request. - * @return boolean - */ - protected boolean isPutOrPost(OperationRequest request) { - return HttpMethod.PUT.equals(request.getMethod()) - || HttpMethod.POST.equals(request.getMethod()); + boolean isPutOrPost() { + return HttpMethod.PUT.equals(this.delegate.getMethod()) + || HttpMethod.POST.equals(this.delegate.getMethod()); + } + + String getBasicAuthCredentials() { + List headerValue = this.delegate.getHeaders() + .get(HttpHeaders.AUTHORIZATION); + if (BasicAuthHeaderFilter.isBasicAuthHeader(headerValue)) { + return BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); + } + return null; + } + + @Override + public byte[] getContent() { + return this.delegate.getContent(); + } + + @Override + public String getContentAsString() { + return this.delegate.getContentAsString(); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders filteredHeaders = new HttpHeaders(); + for (Entry> header : this.delegate.getHeaders().entrySet()) { + if (allowedHeader(header)) { + filteredHeaders.put(header.getKey(), header.getValue()); + } + } + return HttpHeaders.readOnlyHttpHeaders(filteredHeaders); } - /** - * Whether the passed header is allowed according to the configured - * header filters. - * @param header The header to test. - * @return boolean - */ - protected boolean allowedHeader(Map.Entry> header) { + private boolean allowedHeader(Map.Entry> header) { for (HeaderFilter headerFilter : HEADER_FILTERS) { if (!headerFilter.allow(header.getKey(), header.getValue())) { return false; @@ -123,22 +129,24 @@ protected boolean allowedHeader(Map.Entry> header) { return true; } - /** - * Determine if the header passed is a basic auth header. - * @param headerValue The header to test. - * @return boolean - */ - protected boolean isBasicAuthHeader(List headerValue) { - return BasicAuthHeaderFilter.isBasicAuthHeader(headerValue); + @Override + public HttpMethod getMethod() { + return this.delegate.getMethod(); + } + + @Override + public Parameters getParameters() { + return this.delegate.getParameters(); + } + + @Override + public Collection getParts() { + return this.delegate.getParts(); } - /** - * Decodes a basic auth header into name:password credentials. - * @param headerValue The encoded header value. - * @return name:password credentials. - */ - protected String decodeBasicAuthHeader(List headerValue) { - return BasicAuthHeaderFilter.decodeBasicAuthHeader(headerValue); + @Override + public URI getUri() { + return this.delegate.getUri(); } private interface HeaderFilter { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java similarity index 74% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index af4c253c8..5112b6dc5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.cli.curl; +package org.springframework.restdocs.cli; import java.io.PrintWriter; import java.io.StringWriter; @@ -23,14 +23,13 @@ import java.util.Map; import java.util.Map.Entry; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.restdocs.cli.AbstractCliSnippet; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; /** @@ -38,10 +37,10 @@ * * @author Andy Wilkinson * @author Paul-Christian Volkmer - * @see CurlDocumentation#curlRequest() - * @see CurlDocumentation#curlRequest(Map) + * @see CliDocumentation#curlRequest() + * @see CliDocumentation#curlRequest(Map) */ -public class CurlRequestSnippet extends AbstractCliSnippet { +public class CurlRequestSnippet extends TemplatedSnippet { /** * Creates a new {@code CurlRequestSnippet} with no additional attributes. @@ -76,11 +75,12 @@ private String getOptions(Operation operation) { StringWriter command = new StringWriter(); PrintWriter printer = new PrintWriter(command); writeIncludeHeadersInOutputOption(printer); - writeUserOptionIfNecessary(operation.getRequest(), printer); - writeHttpMethodIfNecessary(operation.getRequest(), printer); - writeHeaders(operation.getRequest().getHeaders(), printer); - writePartsIfNecessary(operation.getRequest(), printer); - writeContent(operation.getRequest(), printer); + CliOperationRequest request = new CliOperationRequest(operation.getRequest()); + writeUserOptionIfNecessary(request, printer); + writeHttpMethodIfNecessary(request, printer); + writeHeaders(request, printer); + writePartsIfNecessary(request, printer); + writeContent(request, printer); return command.toString(); } @@ -89,11 +89,10 @@ private void writeIncludeHeadersInOutputOption(PrintWriter writer) { writer.print("-i"); } - private void writeUserOptionIfNecessary(OperationRequest request, + private void writeUserOptionIfNecessary(CliOperationRequest request, PrintWriter writer) { - List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); - if (isBasicAuthHeader(headerValue)) { - String credentials = decodeBasicAuthHeader(headerValue); + String credentials = request.getBasicAuthCredentials(); + if (credentials != null) { writer.print(String.format(" -u '%s'", credentials)); } } @@ -105,12 +104,10 @@ private void writeHttpMethodIfNecessary(OperationRequest request, } } - private void writeHeaders(HttpHeaders headers, PrintWriter writer) { - for (Entry> entry : headers.entrySet()) { - if (allowedHeader(entry)) { - for (String header : entry.getValue()) { - writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); - } + private void writeHeaders(CliOperationRequest request, PrintWriter writer) { + for (Entry> entry : request.getHeaders().entrySet()) { + for (String header : entry.getValue()) { + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } } } @@ -133,7 +130,7 @@ private void writePartsIfNecessary(OperationRequest request, PrintWriter writer) } } - private void writeContent(OperationRequest request, PrintWriter writer) { + private void writeContent(CliOperationRequest request, PrintWriter writer) { String content = request.getContentAsString(); if (StringUtils.hasText(content)) { writer.print(String.format(" -d '%s'", content)); @@ -145,17 +142,18 @@ else if (!request.getParts().isEmpty()) { } } } - else if (isPutOrPost(request)) { + else if (request.isPutOrPost()) { writeContentUsingParameters(request, writer); } } - private void writeContentUsingParameters(OperationRequest request, + private void writeContentUsingParameters(CliOperationRequest request, PrintWriter writer) { - Parameters uniqueParameters = getUniqueParameters(request); + Parameters uniqueParameters = request.getUniqueParameters(); String queryString = uniqueParameters.toQueryString(); if (StringUtils.hasText(queryString)) { writer.print(String.format(" -d '%s'", queryString)); } } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java new file mode 100644 index 000000000..0d132e417 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -0,0 +1,175 @@ +/* + * Copyright 2014-2016 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.cli; + +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.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.StringUtils; + +/** + * A {@link Snippet} that documents the HTTPie command for a request. + * + * @author Raman Gupta + * @author Andy Wilkinson + * @see CliDocumentation#httpieRequest() + * @see CliDocumentation#httpieRequest(Map) + */ +public class HttpieRequestSnippet extends TemplatedSnippet { + + /** + * Creates a new {@code HttpieRequestSnippet} with no additional attributes. + */ + protected HttpieRequestSnippet() { + this(null); + } + + /** + * Creates a new {@code HttpieRequestSnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + protected HttpieRequestSnippet(Map attributes) { + super("httpie-request", attributes); + } + + @Override + protected Map createModel(Operation operation) { + Map model = new HashMap<>(); + CliOperationRequest request = new CliOperationRequest(operation.getRequest()); + model.put("echoContent", getContentStandardIn(request)); + model.put("options", getOptions(request)); + model.put("url", getUrl(request)); + model.put("requestItems", getRequestItems(request)); + return model; + } + + private Object getContentStandardIn(CliOperationRequest request) { + String content = request.getContentAsString(); + if (StringUtils.hasText(content)) { + return String.format("echo '%s' | ", content); + } + return ""; + } + + private String getOptions(CliOperationRequest request) { + StringWriter options = new StringWriter(); + PrintWriter printer = new PrintWriter(options); + writeOptions(request, printer); + writeUserOptionIfNecessary(request, printer); + writeMethodIfNecessary(request, printer); + return options.toString(); + } + + private String getUrl(OperationRequest request) { + return String.format("'%s'", request.getUri()); + } + + private String getRequestItems(CliOperationRequest request) { + StringWriter requestItems = new StringWriter(); + PrintWriter printer = new PrintWriter(requestItems); + writeFormDataIfNecessary(request, printer); + writeHeaders(request, printer); + writeParametersIfNecessary(request, printer); + return requestItems.toString(); + } + + private void writeOptions(CliOperationRequest request, PrintWriter writer) { + if (!request.getParts().isEmpty() || !request.getUniqueParameters().isEmpty()) { + writer.print("--form "); + } + } + + private void writeUserOptionIfNecessary(CliOperationRequest request, + PrintWriter writer) { + String credentials = request.getBasicAuthCredentials(); + if (credentials != null) { + writer.print(String.format("--auth '%s' ", credentials)); + } + } + + private void writeMethodIfNecessary(OperationRequest request, PrintWriter writer) { + writer.print(String.format("%s", request.getMethod().name())); + } + + private void writeFormDataIfNecessary(OperationRequest request, PrintWriter writer) { + for (OperationRequestPart part : request.getParts()) { + writer.printf(" \\%n '%s'", part.getName()); + if (!StringUtils.hasText(part.getSubmittedFileName())) { + // https://github.com/jkbrzt/httpie/issues/342 + writer.printf("@<(echo '%s')", part.getContentAsString()); + } + else { + writer.printf("@'%s'", part.getSubmittedFileName()); + } + } + } + + private void writeHeaders(OperationRequest request, PrintWriter writer) { + HttpHeaders headers = request.getHeaders(); + for (Entry> entry : headers.entrySet()) { + for (String header : entry.getValue()) { + // HTTPie adds Content-Type automatically with --form + if (!request.getParts().isEmpty() + && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) + && header.startsWith(MediaType.MULTIPART_FORM_DATA_VALUE)) { + continue; + } + writer.print(String.format(" '%s:%s'", entry.getKey(), header)); + } + } + } + + private void writeParametersIfNecessary(CliOperationRequest request, + PrintWriter writer) { + if (StringUtils.hasText(request.getContentAsString())) { + return; + } + if (!request.getParts().isEmpty()) { + writeContentUsingParameters(request.getParameters(), writer); + } + else if (request.isPutOrPost()) { + writeContentUsingParameters(request.getUniqueParameters(), writer); + } + } + + private void writeContentUsingParameters(Parameters parameters, PrintWriter writer) { + for (Map.Entry> entry : parameters.entrySet()) { + if (entry.getValue().isEmpty()) { + writer.append(String.format(" '%s='", entry.getKey())); + } + else { + for (String value : entry.getValue()) { + writer.append(String.format(" '%s=%s'", entry.getKey(), value)); + } + } + } + } +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java deleted file mode 100644 index 0313ae027..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieDocumentation.java +++ /dev/null @@ -1,59 +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.cli.httpie; - -import java.util.Map; - -import org.springframework.restdocs.snippet.Snippet; - -/** - * Static factory methods for documenting a RESTful API as if it were being driven using - * the httpie command-line utility. - * - * @author Andy Wilkinson - * @author Paul-Christian Volkmer - * @author Raman Gupta - */ -public abstract class HttpieDocumentation { - - private HttpieDocumentation() { - - } - - /** - * Returns a new {@code Snippet} that will document the httpie request for the API - * operation. - * - * @return the snippet that will document the httpie request - */ - public static Snippet httpieRequest() { - return new HttpieRequestSnippet(); - } - - /** - * Returns a new {@code Snippet} that will document the httpie request for the API - * operation. The given {@code attributes} will be available during snippet - * generation. - * - * @param attributes the attributes - * @return the snippet that will document the httpie request - */ - public static Snippet httpieRequest(Map attributes) { - return new HttpieRequestSnippet(attributes); - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java deleted file mode 100644 index 7e251a90e..000000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippet.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2014-2016 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.cli.httpie; - -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.http.HttpHeaders; -import org.springframework.restdocs.cli.AbstractCliSnippet; -import org.springframework.restdocs.operation.Operation; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.Parameters; -import org.springframework.restdocs.snippet.Snippet; -import org.springframework.util.StringUtils; - -/** - * A {@link Snippet} that documents the httpie command for a request. - * - * @author Raman Gupta - * @see HttpieDocumentation#httpieRequest() - * @see HttpieDocumentation#httpieRequest(Map) - */ -public class HttpieRequestSnippet extends AbstractCliSnippet { - - /** - * Creates a new {@code CurlRequestSnippet} with no additional attributes. - */ - protected HttpieRequestSnippet() { - this(null); - } - - /** - * Creates a new {@code CurlRequestSnippet} with the given additional - * {@code attributes} that will be included in the model during template rendering. - * - * @param attributes The additional attributes - */ - protected HttpieRequestSnippet(Map attributes) { - super("httpie-request", attributes); - } - - @Override - protected Map createModel(final Operation operation) { - Map model = new HashMap<>(); - model.put("echo_content", getContentStdIn(operation)); - model.put("options", getOptions(operation)); - model.put("url", getUrl(operation)); - model.put("request_items", getRequestItems(operation)); - return model; - } - private Object getContentStdIn(final Operation operation) { - OperationRequest request = operation.getRequest(); - String content = request.getContentAsString(); - if (StringUtils.hasText(content)) { - return String.format("echo '%s' | ", content); - } - else { - return ""; - } - } - - private String getOptions(Operation operation) { - StringWriter options = new StringWriter(); - PrintWriter printer = new PrintWriter(options); - writeOptions(operation.getRequest(), printer); - writeUserOptionIfNecessary(operation.getRequest(), printer); - writeMethodIfNecessary(operation.getRequest(), printer); - return options.toString(); - } - - private String getUrl(Operation operation) { - return String.format("'%s'", operation.getRequest().getUri()); - } - - private String getRequestItems(final Operation operation) { - StringWriter requestItems = new StringWriter(); - PrintWriter printer = new PrintWriter(requestItems); - writeFormDataIfNecessary(operation.getRequest(), printer); - writeHeaders(operation.getRequest(), printer); - writeContent(operation.getRequest(), printer); - return requestItems.toString(); - } - private void writeOptions(OperationRequest request, PrintWriter writer) { - Parameters uniqueParameters = getUniqueParameters(request); - if (request.getParts().size() > 0 || uniqueParameters.size() > 0) { - writer.print("--form "); - } - } - - private void writeUserOptionIfNecessary(OperationRequest request, - PrintWriter writer) { - List headerValue = request.getHeaders().get(HttpHeaders.AUTHORIZATION); - if (isBasicAuthHeader(headerValue)) { - String credentials = decodeBasicAuthHeader(headerValue); - writer.print(String.format("--auth '%s' ", credentials)); - } - } - - private void writeMethodIfNecessary(OperationRequest request, PrintWriter writer) { - writer.print(String.format("%s", request.getMethod().name())); - } - - private void writeFormDataIfNecessary(OperationRequest request, PrintWriter writer) { - for (OperationRequestPart part : request.getParts()) { - writer.printf(" \\\n '%s'", part.getName()); - if (!StringUtils.hasText(part.getSubmittedFileName())) { - // httpie https://github.com/jkbrzt/httpie/issues/342 - writer.printf("@<(echo '%s')", part.getContentAsString()); - } - else { - writer.printf("@'%s'", part.getSubmittedFileName()); - } - // httpie does not currently support manually set content type by part - } - } - - private void writeHeaders(OperationRequest request, PrintWriter writer) { - HttpHeaders headers = request.getHeaders(); - for (Entry> entry : headers.entrySet()) { - if (allowedHeader(entry)) { - for (String header : entry.getValue()) { - // form Content-Type not required, added automatically by httpie with --form - if (request.getParts().size() > 0 - && entry.getKey().equals(HttpHeaders.CONTENT_TYPE) - && header.startsWith("multipart/form-data")) { - continue; - } - writer.print(String.format(" '%s:%s'", entry.getKey(), header)); - } - } - } - } - - private void writeContent(OperationRequest request, PrintWriter writer) { - String content = request.getContentAsString(); - if (!StringUtils.hasText(content)) { - if (!request.getParts().isEmpty()) { - for (Entry> entry : request.getParameters().entrySet()) { - for (String value : entry.getValue()) { - writer.print(String.format(" '%s=%s'", entry.getKey(), value)); - } - } - } - else if (isPutOrPost(request)) { - writeContentUsingParameters(request, writer); - } - } - } - - private void writeContentUsingParameters(OperationRequest request, - PrintWriter writer) { - Parameters uniqueParameters = getUniqueParameters(request); - for (Map.Entry> entry : uniqueParameters.entrySet()) { - if (entry.getValue().isEmpty()) { - writer.append(String.format(" '%s='", entry.getKey())); - } - else { - for (String value : entry.getValue()) { - writer.append(String.format(" '%s=%s'", entry.getKey(), value)); - } - } - } - } -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/package-info.java similarity index 75% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/package-info.java index 2bdf5df66..f06c96709 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/httpie/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -15,6 +15,6 @@ */ /** - * Documenting the httpie command required to make a request to a RESTful API. + * Documenting CLI commands required to make a request to a RESTful API. */ -package org.springframework.restdocs.cli.httpie; +package org.springframework.restdocs.cli; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 7c2b459c7..cb72f152c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -21,8 +21,7 @@ import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.cli.curl.CurlDocumentation; -import org.springframework.restdocs.cli.httpie.HttpieDocumentation; +import org.springframework.restdocs.cli.CliDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; import org.springframework.restdocs.snippet.Snippet; @@ -39,12 +38,9 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer { - private List defaultSnippets = Arrays.asList( - CurlDocumentation.curlRequest(), - HttpieDocumentation.httpieRequest(), - HttpDocumentation.httpRequest(), - HttpDocumentation.httpResponse() - ); + private List defaultSnippets = Arrays.asList(CliDocumentation.curlRequest(), + CliDocumentation.httpieRequest(), HttpDocumentation.httpRequest(), + HttpDocumentation.httpResponse()); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index a9b094f6c..123c7a663 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -24,8 +24,8 @@ * Static factory methods for documenting a RESTful API as if it were being driven using * the cURL command-line utility. * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. - * + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CliDocumentation}. * @author Andy Wilkinson * @author Yann Le Guern * @author Dmitriy Mayboroda @@ -44,8 +44,10 @@ private CurlDocumentation() { * * @return the snippet that will document the curl request * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CliDocumentation#curlRequest()}. */ + @Deprecated public static Snippet curlRequest() { return new CurlRequestSnippet(); } @@ -58,8 +60,10 @@ public static Snippet curlRequest() { * @param attributes the attributes * @return the snippet that will document the curl request * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlDocumentation}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CliDocumentation#curlRequest(Map)}. */ + @Deprecated public static Snippet curlRequest(Map attributes) { return new CurlRequestSnippet(attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 29396bac6..18148392a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -23,16 +23,23 @@ /** * A {@link Snippet} that documents the curl command for a request. * + * @author Andy Wilkinson + * @author Paul-Christian Volkmer * @author Raman Gupta - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CurlRequestSnippet}. */ -public class CurlRequestSnippet extends org.springframework.restdocs.cli.curl.CurlRequestSnippet { +@Deprecated +public class CurlRequestSnippet + extends org.springframework.restdocs.cli.CurlRequestSnippet { /** * Creates a new {@code CurlRequestSnippet} with no additional attributes. * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CurlRequestSnippet}. */ + @Deprecated protected CurlRequestSnippet() { super(); } @@ -41,8 +48,10 @@ protected CurlRequestSnippet() { * Creates a new {@code CurlRequestSnippet} with additional attributes. * @param attributes The additional attributes. * - * @deprecated Since 1.1 in favor of {@link org.springframework.restdocs.cli.curl.CurlRequestSnippet}. + * @deprecated Since 1.1 in favor of + * {@link org.springframework.restdocs.cli.CurlRequestSnippet}. */ + @Deprecated protected CurlRequestSnippet(final Map attributes) { super(attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java similarity index 83% rename from spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java rename to spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java index 02ebe62cd..e614e2b84 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/curl/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/package-info.java @@ -16,5 +16,8 @@ /** * Documenting the curl command required to make a request to a RESTful API. + * + * @deprecated Since 1.1 in favor of functionality in + * {@code org.springframework.restdocs.cli} */ -package org.springframework.restdocs.cli.curl; +package org.springframework.restdocs.curl; diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet index 05771238f..1b690a5f5 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-httpie-request.snippet @@ -1,4 +1,4 @@ [source,bash] ---- -$ {{echo_content}}http {{options}} {{url}}{{request_items}} +$ {{echoContent}}http {{options}} {{url}}{{requestItems}} ---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet index acddb53f0..65d78f740 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-httpie-request.snippet @@ -1,3 +1,3 @@ ```bash -$ {{echo_content}}http {{options}} {{url}}{{request_items}} +$ {{echoContent}}http {{options}} {{url}}{{requestItems}} ``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java similarity index 99% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index 0d8bb6324..bb7cff803 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.cli.curl; +package org.springframework.restdocs.cli; import java.io.IOException; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java similarity index 96% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 1fb2abd7a..6194fef91 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/httpie/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.cli.httpie; +package org.springframework.restdocs.cli; import java.io.IOException; @@ -156,8 +156,8 @@ public void postRequestWithMultipleParameters() throws IOException { @Test public void postRequestWithUrlEncodedParameter() throws IOException { this.snippet.expectHttpieRequest("post-request-with-url-encoded-parameter") - .withContents(codeBlock("bash").content( - "$ http --form POST 'http://localhost/foo' 'k1=a&b'")); + .withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() .document(operationBuilder("post-request-with-url-encoded-parameter") .request("http://localhost/foo").method("POST").param("k1", "a&b") @@ -214,8 +214,8 @@ public void putRequestWithMultipleParameters() throws IOException { @Test public void putRequestWithUrlEncodedParameter() throws IOException { this.snippet.expectHttpieRequest("put-request-with-url-encoded-parameter") - .withContents(codeBlock("bash").content( - "$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); + .withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() .document(operationBuilder("put-request-with-url-encoded-parameter") .request("http://localhost/foo").method("PUT").param("k1", "a&b") @@ -224,8 +224,8 @@ public void putRequestWithUrlEncodedParameter() throws IOException { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectHttpieRequest("request-with-headers") - .withContents(codeBlock("bash").content("$ http GET 'http://localhost/foo'" + this.snippet.expectHttpieRequest("request-with-headers").withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo'" + " 'Content-Type:application/json' 'a:alpha'")); new HttpieRequestSnippet().document( operationBuilder("request-with-headers").request("http://localhost/foo") @@ -298,7 +298,7 @@ public void multipartPostWithParameters() throws IOException { } @Test - public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { + public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { this.snippet.expectHttpieRequest("basic-auth").withContents(codeBlock("bash") .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); new HttpieRequestSnippet() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 7a36e3200..7198ffc85 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -24,9 +24,9 @@ import org.junit.Test; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.restdocs.cli.curl.CurlDocumentation; -import org.springframework.restdocs.cli.curl.CurlRequestSnippet; -import org.springframework.restdocs.cli.httpie.HttpieRequestSnippet; +import org.springframework.restdocs.cli.CliDocumentation; +import org.springframework.restdocs.cli.CurlRequestSnippet; +import org.springframework.restdocs.cli.HttpieRequestSnippet; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; @@ -108,7 +108,7 @@ public void customWriterResolver() { public void customDefaultSnippets() { RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); - this.configurer.snippets().withDefaults(CurlDocumentation.curlRequest()) + this.configurer.snippets().withDefaults(CliDocumentation.curlRequest()) .apply(configuration, context); assertThat(configuration, hasEntry( diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet index 0bff58b19..7c4eb0483 100644 --- a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/httpie-request-with-title.snippet @@ -1,5 +1,5 @@ [source,bash] .{{title}} ---- -$ {{echo_content}}http {{options}} {{url}}{{request_items}} +$ {{echoContent}}http {{options}} {{url}}{{requestItems}} ---- \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet index 29beade11..67dff4550 100644 --- a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/httpie-request-with-title.snippet @@ -1,4 +1,4 @@ {{title}} ```bash -$ {{echo_content}}http {{options}} {{url}}{{request_items}} +$ {{echoContent}}http {{options}} {{url}}{{requestItems}} ``` \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index ad06b187b..51c35ee4d 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -56,7 +56,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; -import static org.springframework.restdocs.cli.curl.CurlDocumentation.curlRequest; +import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -171,6 +171,38 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { + "-H 'Accept: application/json' -d 'a=alpha'")))); } + @Test + public void httpieSnippetWithContent() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(post("/").accept(MediaType.APPLICATION_JSON).content("content")) + .andExpect(status().isOk()) + .andDo(document("httpie-snippet-with-content")); + assertThat( + new File( + "build/generated-snippets/httpie-snippet-with-content/httpie-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ echo 'content' | http POST 'http://localhost:8080/'" + + " 'Accept:application/json'")))); + } + + @Test + public void httpieSnippetWithQueryStringOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + mockMvc.perform(post("/?foo=bar").param("foo", "bar").param("a", "alpha") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("httpie-snippet-with-query-string")); + assertThat( + new File( + "build/generated-snippets/httpie-snippet-with-query-string/httpie-request.adoc"), + is(snippet(asciidoctor()) + .withContents(codeBlock(asciidoctor(), "bash").content("$ http " + + "--form POST 'http://localhost:8080/?foo=bar' " + + "'Accept:application/json' 'a=alpha'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) From 6ea6dcf99663edaff1bdb82a64f2086f2fcb3546 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 9 Mar 2016 17:51:48 +0000 Subject: [PATCH 076/898] Fix test failures on Windows caused by different new line characters --- .../cli/HttpieRequestSnippetTests.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 6194fef91..3fd92c383 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -236,8 +236,9 @@ public void requestWithHeaders() throws IOException { @Test public void multipartPostWithNoSubmittedFileName() throws IOException { - String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" - + " 'metadata'@<(echo '{\"description\": \"foo\"}')"; + String expectedContent = String + .format("$ http --form POST 'http://localhost/upload' \\%n" + + " 'metadata'@<(echo '{\"description\": \"foo\"}')"); this.snippet.expectHttpieRequest("multipart-post-no-original-filename") .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet() @@ -252,8 +253,9 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { @Test public void multipartPostWithContentType() throws IOException { // httpie does not yet support manually set content type by part - String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" - + " 'image'@'documents/images/example.png'"; + String expectedContent = String + .format("$ http --form POST 'http://localhost/upload' \\%n" + + " 'image'@'documents/images/example.png'"); this.snippet.expectHttpieRequest("multipart-post-with-content-type") .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet() @@ -268,8 +270,9 @@ public void multipartPostWithContentType() throws IOException { @Test public void multipartPost() throws IOException { - String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" - + " 'image'@'documents/images/example.png'"; + String expectedContent = String + .format("$ http --form POST 'http://localhost/upload' \\%n" + + " 'image'@'documents/images/example.png'"); this.snippet.expectHttpieRequest("multipart-post") .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet() @@ -283,8 +286,10 @@ public void multipartPost() throws IOException { @Test public void multipartPostWithParameters() throws IOException { - String expectedContent = "$ http --form POST 'http://localhost/upload' \\\n" - + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado' 'b=banana'"; + String expectedContent = String + .format("$ http --form POST 'http://localhost/upload' \\%n" + + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado'" + + " 'b=banana'"); this.snippet.expectHttpieRequest("multipart-post-with-parameters") .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet() From 488d36d4bfea50ea0bd28026d10e7a3c5a0120c4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 10:47:11 +0100 Subject: [PATCH 077/898] Do not double-encode URIs when they are being modified Previously, when UriModifyingOperationPreprocessor modified a URI it would always encode it. However, the URIs being modified will always have been encoded, resulting in double-encoding of the URI. This commit updates UriModifyingOperationPreprocessor to tell UriComponentsBuilder that the components have already been encoded. This ensures that they are not encoded for a second time when the URI is being built. Closes gh-211 --- .../preprocess/UriModifyingOperationPreprocessor.java | 2 +- .../UriModifyingOperationPreprocessorTests.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index b06530b6c..9063ac948 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -140,7 +140,7 @@ public OperationRequest preprocess(OperationRequest request) { modifiedHeaders.set(HttpHeaders.HOST, this.host); } return this.contentModifyingDelegate.preprocess( - new OperationRequestFactory().create(uriBuilder.build().toUri(), + new OperationRequestFactory().create(uriBuilder.build(true).toUri(), request.getMethod(), request.getContent(), modifiedHeaders, request.getParameters(), modify(request.getParts()))); } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java index 30677718b..60bda2db8 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -307,6 +307,16 @@ public void urisInRequestPartContentCanBeModified() { is(equalTo("The uri 'http://api.example.com:12345' should be used"))); } + @Test + public void modifiedUriDoesNotGetDoubleEncoded() { + this.preprocessor.scheme("https"); + OperationRequest processed = this.preprocessor + .preprocess(createRequestWithUri("http://localhost:12345?foo=%7B%7D")); + assertThat(processed.getUri(), + is(equalTo(URI.create("https://localhost:12345?foo=%7B%7D")))); + + } + private OperationRequest createRequestWithUri(String uri) { return this.requestFactory.create(URI.create(uri), HttpMethod.GET, new byte[0], new HttpHeaders(), new Parameters(), From cb39af5163ce1899129baf479e1f606c3348fe21 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 11:12:15 +0100 Subject: [PATCH 078/898] Improve the formatting of the default Markdown path parameters snippet Add the required blank line between the table "title" and the table itself. Without this blank line the table wasn't not formatted correctly. The "title" has also been wrapped in back ticks to improve its formatting in the generated HTML. Closes gh-212 --- .../markdown/default-path-parameters.snippet | 3 ++- .../restdocs/request/PathParametersSnippetTests.java | 12 +++++++++--- .../restdocs/test/SnippetMatchers.java | 1 + 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet index f5f1daaee..f1811879f 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet @@ -1,4 +1,5 @@ -{{path}} +`{{path}}` + Parameter | Description --------- | ----------- {{#parameters}} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 7bee0c142..1b50d3d17 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -25,6 +25,7 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -49,7 +50,7 @@ public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void pathParameters() throws IOException { this.snippet.expectPathParameters("path-parameters").withContents( - tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) @@ -61,7 +62,7 @@ public void pathParameters() throws IOException { @Test public void ignoredPathParameter() throws IOException { this.snippet.expectPathParameters("ignored-path-parameter").withContents( - tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", + tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) @@ -74,7 +75,7 @@ public void ignoredPathParameter() throws IOException { public void pathParametersWithQueryString() throws IOException { this.snippet.expectPathParameters("path-parameters-with-query-string") .withContents( - tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))).document( @@ -132,4 +133,9 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { .build()); } + private String getTitle() { + return this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}/{b}" + : "`/{a}/{b}`"; + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index a7875b76d..bb0c146cd 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -345,6 +345,7 @@ private MarkdownTableMatcher(String title, String... columns) { super(TemplateFormats.asciidoctor()); if (StringUtils.hasText(title)) { this.addLine(title); + this.addLine(""); } String header = StringUtils .collectionToDelimitedString(Arrays.asList(columns), " | "); From 05f738d12c04891d42f13a90fdf2649765f6750b Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 4 Mar 2016 17:54:01 +0900 Subject: [PATCH 079/898] Fix typo --- .../restdocs/templates/mustache/MustacheTemplateEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index fee0f6af9..8079060ad 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -52,7 +52,7 @@ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) /** * Creates a new {@code MustacheTemplateEngine} that will use the given - * {@code tempalteResourceResolver} to resolve templates and the given + * {@code templateResourceResolver} to resolve templates and the given * {@code compiler} to compile them. * * @param templateResourceResolver the resolver to use From 547c4f3fb14777014103faa9f3d3e29c82d19742 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 11:32:15 +0100 Subject: [PATCH 080/898] Simplify documentation of Asciidoctor Maven plugin's phase Previously the phase was documented as `package` by default with instructions to change it to `prepare-package` if you want to include the documentation in the project's package. This led to some confusion and sometimes the step to change it to `prepare-package` was missed. This commit updates the documentation to recommend that `prepare-package` is always used. It works equally well for both cases (packaging the documentation or not) and avoids the possible confusion described above. Closes gh-218 --- docs/src/docs/asciidoc/getting-started.adoc | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 0232564be..4a12eef42 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -134,7 +134,7 @@ described below. generate-docs - package <6> + prepare-package <6> process-asciidoc @@ -159,9 +159,8 @@ described below. <4> Add the Asciidoctor plugin <5> Define an attribute named `snippets` that can be used when including the generated snippets in your documentation. -<6> [[getting-started-build-configuration-maven-plugin-phase]] If you want to -<> in your -project's jar you should use the `prepare-package` phase. +<6> Using `prepare-package` allows the documentation to be + <>. [[getting-started-build-configuration-maven-packaging]] ==== Packaging the documentation @@ -170,10 +169,8 @@ You may want to package the generated documentation in your project's jar file, example to have it {spring-boot-docs}/#boot-features-spring-mvc-static-content[served as static content] by Spring Boot. -First, configure the Asciidoctor plugin so that it runs in the `prepare-package` phase, as -<>. Now configure -Maven's resources plugin to copy the generated documentation into a location where it'll -be included in the project's jar: +Configure Maven's resources plugin to copy the generated documentation into a location +where it'll be included in the project's jar: [source,xml,indent=0] ---- From 83e8b92c3bf4f21be895b3cbede19dab67eedea2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 11:55:52 +0100 Subject: [PATCH 081/898] Upgrade to Gradle 2.12 --- gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53638 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 4 ++-- gradlew.bat | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2..5ccda13e9cb94678ba179b32452cf3d60dc36353 100644 GIT binary patch delta 2519 zcmZWq3s6%>6y3ap5G9EaCBl3x(7|aNRC@L0#DcC_; z3Tw3%JBXm5r9&XGs6|EjiG-huHC_j1vRZ-3oKH?YN^>Xh-d@Gn9WxLNe-!HL2A2C|q6eYgY6&L?;NIB9bvo?pxcKtht%b!X{CZ?8fY-8V2B$^vPz($g-g*=?MY^7ZRVijR%{Qm@_qJq5sN z;iS<{E8U~2<7`S!O$0k_ziv63>~gk~#m3Ktvb?amLcWYKr8&Cx;40LumFTqvFt~dz zw5+#yXQwGcr*>Lk{agWa|@ZT&(oX44`mfIQ+yz>WZzf^okh^eZtr&P^T2 zqh|5RIc758oXfP&CH7-na&(L)_B{?GG@*)?iG3K=XRtsZcYRkgJ}YKRDPr}Y*M5SI zMt!C=)L!NnwPVKoLfyCA#vH2Wgv=OYnYz_yTgz(`N1twedetxgX2!K)41WyiGB*J8G zkpgaL6l1bMvT0QTQXWC8&Q-RNVsem_hE)w>e4)Tg4Ky`sit(WaXM$Nb($su8;WkZF0n>RJUs9-9YZ`Eznux zRFLV-nlgsPO*9d{fCPT<$M|5AJ#PM;4zpdyP(^y_yNx{vmCf#$lb}fWR?h?AGd=*` zoJmq(MsonsZfmBg^mRzv8VXuu*xcm3iE_A$e=8EcigbV*Tg2~Nm?6-7g3hb8QcrlIduF~Qedr76c4=ojPi;s3WDbEraAOtY>)>-7dzx0K^r z7K;+>p*nUB9G?)9w9?e9cvw>94A;lY;h;gRU_=J&C9eZe>48Ydog)IaA`vU7ZMDZl z&gI1+9@4;S%`$kgjVM}el~=bTwR_0cA`bCv3Z8x}#dLD|zRf*Oj}dtrx)zsl$ahn5 z4Hmm>CY9~>v~DOwyZML&07nklA`N>DP7}jln~9C1nuyMFRM^S))^sof+xWT7nzXp1 zji#Pw!JTn)**=len*jt2sl8-Pu zxU^k{8y7j_@9n*gwcWs9HmtE)}tz&_z>2 k=SYvvn=#vf;MWiyP>1np-Hup~`v)^6sL#9)gSx%{1J}&t-T(jq delta 2539 zcmZWq2~bl<7~VWWh#I0q3E?$BgaRTSA%JpdJu6n^Qt-Ay2ZRCY0SFef5-d6bEy`9$ z99p%AVhROJl-i<#K)3^_c#O6l^`MG?c7{r;*nPWC;-i^)*?r%B|F_@2`|rLzg(iCn zO~k?eCj8L=2!$Zqf12L`v7^uXNZmid1ryhz}I5zNyHK8|Q}Che=~ zxP`8~d$^+=OC?YQ%H%&L&}P#@0%@f}iGW~J#5Y+?XffTCV7}5^7Bk4g%n0VJ2pdm& z>F#4#Uaptr7=jfq7#J;P{M<5FY(FLdKqrQ|d>Z!G31EO;sJ#}b5lEQvSVio99S?vN zCIEN=sMh;vo5L>hh)T}NP$ACWI%(^(5R`uqe7<2mEexC*tn>&`mg`h!%2ldmTawMi zAxf3f;}KjO?qZ&dWOX;L%`>0?8h#mWx63oPZBwmc+*f^7ODy^4YVKD%RxSyfwlB3* zy2^KB+tI`hj7s^6<8$7W=*suCyx;75>;Ovs(H>{q=VbU+z9@ zKO^(rvr|`-V@}?C**#a&q;$FI5#C-pUmvi$N%NvOHNn4Vk+9gNx4>Y1C&S}W{;Pz2 z<%ia<)ud(=DbCl5?;g$$3Q6=?Hc;!6*F0!3^m6OJpZ5=>HK#R;=gT+d9KPTf7nq_8 zn=^BN{Dz|kO2dcd99t99b`ZYte#kf7=GQRu5*09LV%&KqRhSy#%4a&-(-C_q+=CvG z-2_rZ4N+odu&N|Lb$|k3w<-L-Y^rv3Oa-B8#c_E&!aWghDu=Yi{rv|mf;)$7Cd6cKp#rX{6Jc_z`KODRLLEex23OfejLAV_8kW_H$i`f% zrK!Pj81JujA(%}aO-+$vEU->Suvmm$lA)!uheI*}{HczOzld;|0;ML019cL@71h(! zR(BjfyWR<#T713o!>h=>gA9O4oD9;vSW{;3Xgy5?E+B#Y1sLzGcfgBpyaq=uEQCtE z8|HAYLgOqtbs|jH`w{IHJx%RgiL`AXzd?fIhA&4H!rlBX=-HqeaUUi%c;hn#D;j8C z9Bgf{C9k~=G<75bDh<}KBZ7vr8$|>UXrw8}NEnf853?I5D42t^Z2l;lVzfnnqb-_c z^a*roizrMCj*`PS?*z=7Xli^6#?zb4;OZDD>}?fcp>7BBmwxE-_eWxG9HFUiEHnKL8L##|o7Uu^0?s!F!08{PVoCWV@ z%`)yyRx3>%%E6+Ittuiogs_FV2%7}=wG&4h#(s3A5asHEa^;@CzT-G}f14einR6L) zX8z;EuyvOrk?Z?+8dDM5ogRdSqpN@(1As3lP!FdYmJnw<4K%f@1f@D{c&*M=mnR-Z z&NOJdxciz}Hawxc-5$Gh<}Vgmqjia9QsGRp{R|?@p(uMSvo@H#pi}zzDO$iC$Pu?j zZ)&mfjrK`)jCBZn^O@Ctja0oks&Z)Cv6xto?Vzc?^EhtWFA}CM2;PkFMO7Hz-zI?K T&WTu)`=v38>eP2&V5irA_39c# diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c9568aee..0b95e5d4b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 15 17:15:12 GMT 2016 +#Mon Apr 04 11:55:32 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-bin.zip diff --git a/gradlew b/gradlew index 97fac783e..9d82f7891 100755 --- a/gradlew +++ b/gradlew @@ -56,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar diff --git a/gradlew.bat b/gradlew.bat index 8a0b282aa..5f192121e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args From feb2f352f69200c0b4fd098c1d8017b065c7da69 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 4 Apr 2016 13:26:41 +0100 Subject: [PATCH 082/898] Fix compiler warnings --- build.gradle | 4 +++ .../mockmvc/CustomDefaultSnippets.java | 1 + .../com/example/mockmvc/CustomEncoding.java | 2 +- .../com/example/mockmvc/CustomFormat.java | 2 +- .../mockmvc/CustomUriConfiguration.java | 2 +- .../mockmvc/EveryTestPreprocessing.java | 1 - .../ExampleApplicationTestNgTests.java | 5 ++-- .../mockmvc/ExampleApplicationTests.java | 7 +++-- .../example/mockmvc/ParameterizedOutput.java | 2 +- .../restassured/CustomDefaultSnippets.java | 1 + .../example/restassured/CustomEncoding.java | 2 +- .../com/example/restassured/CustomFormat.java | 4 +-- .../restassured/EveryTestPreprocessing.java | 1 - .../ExampleApplicationTestNgTests.java | 1 + .../restassured/ExampleApplicationTests.java | 2 +- .../restassured/ParameterizedOutput.java | 2 +- .../snippet/StandardWriterResolver.java | 1 + .../RestDocumentationConfigurerTests.java | 29 +++++++++++-------- ...tationContextPlaceholderResolverTests.java | 17 +++++++---- .../snippet/StandardWriterResolverTests.java | 22 +++++++------- .../restdocs/test/OperationBuilder.java | 12 ++++++-- .../mockmvc/MockMvcRestDocumentation.java | 4 +-- 22 files changed, 77 insertions(+), 47 deletions(-) diff --git a/build.gradle b/build.gradle index c6637e331..f1d171285 100644 --- a/build.gradle +++ b/build.gradle @@ -90,6 +90,10 @@ subprojects { } } + compileJava { + options.compilerArgs = [ '-Xlint:deprecation', '-Xlint:-options', '-Werror' ] + } + } configure(subprojects - project(":docs")) { subproject -> diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 4fb2990f8..6c974927c 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -37,6 +37,7 @@ public class CustomDefaultSnippets { @Autowired private WebApplicationContext context; + @SuppressWarnings("unused") private MockMvc mockMvc; @Before diff --git a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java index 9352ca95e..0351216aa 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -20,7 +20,6 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -35,6 +34,7 @@ public class CustomEncoding { @Autowired private WebApplicationContext context; + @SuppressWarnings("unused") private MockMvc mockMvc; @Before diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index 2415bd40c..b0cae3b18 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -20,7 +20,6 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -36,6 +35,7 @@ public class CustomFormat { @Autowired private WebApplicationContext context; + @SuppressWarnings("unused") private MockMvc mockMvc; @Before diff --git a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index b091199a8..bc3e0e75f 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -20,7 +20,6 @@ import org.junit.Rule; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -35,6 +34,7 @@ public class CustomUriConfiguration { @Autowired private WebApplicationContext context; + @SuppressWarnings("unused") private MockMvc mockMvc; @Before diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index 85e7e9979..18799410e 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java index 0018c467b..fac8c5ca8 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java @@ -32,12 +32,13 @@ public class ExampleApplicationTestNgTests { public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( "target/generated-snippets"); + @SuppressWarnings("unused") // tag::setup[] + private MockMvc mockMvc; + @Autowired private WebApplicationContext context; - private MockMvc mockMvc; - @BeforeMethod public void setUp(Method method) { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index a27725f04..21afd53d3 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -18,6 +18,7 @@ import org.junit.Before; import org.junit.Rule; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.web.servlet.MockMvc; @@ -31,12 +32,14 @@ public class ExampleApplicationTests { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "target/generated-snippets"); + + @SuppressWarnings("unused") // tag::setup[] + private MockMvc mockMvc; + @Autowired private WebApplicationContext context; - private MockMvc mockMvc; - @Before public void setUp() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) diff --git a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index c5e6a362a..d39102bff 100644 --- a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -32,6 +31,7 @@ public class ParameterizedOutput { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + @SuppressWarnings("unused") private MockMvc mockMvc; private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 57c65d360..27512e649 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -32,6 +32,7 @@ public class CustomDefaultSnippets { public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build"); + @SuppressWarnings("unused") private RequestSpecification spec; @Before diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java index 09039a448..6e5c6e867 100644 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; @@ -31,6 +30,7 @@ public class CustomEncoding { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + @SuppressWarnings("unused") private RequestSpecification spec; @Before diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index 36ba445d4..c0d8adb7e 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.templates.TemplateFormats; import com.jayway.restassured.builder.RequestSpecBuilder; @@ -31,7 +30,8 @@ public class CustomFormat { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); - + + @SuppressWarnings("unused") private RequestSpecification spec; @Before diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index a4ea45880..e143bcc26 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.restassured.RestDocumentationFilter; import com.jayway.restassured.RestAssured; diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java index a28e54aec..aa0a57966 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java @@ -32,6 +32,7 @@ public class ExampleApplicationTestNgTests { private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( "build/generated-snippets"); + @SuppressWarnings("unused") // tag::setup[] private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java index 33840cb70..c7b8d774a 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; @@ -32,6 +31,7 @@ public class ExampleApplicationTests { public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); + @SuppressWarnings("unused") // tag::setup[] private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java index 29c24835a..88db51d8f 100644 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -19,7 +19,6 @@ import org.junit.Before; import org.junit.Rule; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.RestDocumentation; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; @@ -33,6 +32,7 @@ public class ParameterizedOutput { public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); + @SuppressWarnings("unused") private RequestSpecification spec; // tag::parameterized-output[] diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 9faf267dd..479407ea0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -96,6 +96,7 @@ public Writer resolve(String operationName, String snippetName, } @Override + @Deprecated public void setEncoding(String encoding) { this.encoding = encoding; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 7198ffc85..a629283f2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -23,6 +23,7 @@ import org.hamcrest.Matchers; import org.junit.Test; +import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.cli.CliDocumentation; import org.springframework.restdocs.cli.CurlRequestSnippet; @@ -57,9 +58,8 @@ public class RestDocumentationConfigurerTests { @SuppressWarnings("unchecked") @Test public void defaultConfiguration() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); - this.configurer.apply(configuration, context); + this.configurer.apply(configuration, createContext()); assertThat(configuration, hasEntry(equalTo(TemplateEngine.class.getName()), instanceOf(MustacheTemplateEngine.class))); assertThat(configuration, hasEntry(equalTo(WriterResolver.class.getName()), @@ -86,30 +86,29 @@ public void defaultConfiguration() { @Test public void customTemplateEngine() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); TemplateEngine templateEngine = mock(TemplateEngine.class); - this.configurer.templateEngine(templateEngine).apply(configuration, context); + this.configurer.templateEngine(templateEngine).apply(configuration, + createContext()); assertThat(configuration, Matchers.hasEntry( TemplateEngine.class.getName(), templateEngine)); } @Test public void customWriterResolver() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); WriterResolver writerResolver = mock(WriterResolver.class); - this.configurer.writerResolver(writerResolver).apply(configuration, context); + this.configurer.writerResolver(writerResolver).apply(configuration, + createContext()); assertThat(configuration, Matchers.hasEntry( WriterResolver.class.getName(), writerResolver)); } @Test public void customDefaultSnippets() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); this.configurer.snippets().withDefaults(CliDocumentation.curlRequest()) - .apply(configuration, context); + .apply(configuration, createContext()); assertThat(configuration, hasEntry( equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), @@ -122,10 +121,9 @@ public void customDefaultSnippets() { @Test public void customSnippetEncoding() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); this.configurer.snippets().withEncoding("ISO 8859-1").apply(configuration, - context); + createContext()); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), instanceOf(SnippetConfiguration.class))); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration @@ -135,10 +133,9 @@ public void customSnippetEncoding() { @Test public void customTemplateFormat() { - RestDocumentationContext context = new RestDocumentationContext(null, null, null); Map configuration = new HashMap<>(); this.configurer.snippets().withTemplateFormat(TemplateFormats.markdown()) - .apply(configuration, context); + .apply(configuration, createContext()); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), instanceOf(SnippetConfiguration.class))); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration @@ -147,6 +144,14 @@ public void customTemplateFormat() { is(equalTo(TemplateFormats.markdown()))); } + private RestDocumentationContext createContext() { + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + "build"); + manualRestDocumentation.beforeTest(null, null); + RestDocumentationContext context = manualRestDocumentation.beforeOperation(); + return context; + } + private static final class TestRestDocumentationConfigurer extends RestDocumentationConfigurer { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index def767471..83f96e611 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -18,6 +18,7 @@ import org.junit.Test; +import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -70,17 +71,23 @@ public void camelCaseClassName() throws Exception { @Test public void stepCount() throws Exception { - assertThat(createResolver("stepCount").resolvePlaceholder("step"), equalTo("0")); + assertThat(createResolver("stepCount").resolvePlaceholder("step"), equalTo("1")); } private PlaceholderResolver createResolver() { - return new RestDocumentationContextPlaceholderResolver( - new RestDocumentationContext(getClass(), null, null)); + return createResolver(null); } private PlaceholderResolver createResolver(String methodName) { - return new RestDocumentationContextPlaceholderResolver( - new RestDocumentationContext(getClass(), methodName, null)); + return new RestDocumentationContextPlaceholderResolver(createContext(methodName)); + } + + private RestDocumentationContext createContext(String methodName) { + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + "build"); + manualRestDocumentation.beforeTest(getClass(), methodName); + RestDocumentationContext context = manualRestDocumentation.beforeOperation(); + return context; } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 56c2b573a..af5ccd149 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -20,11 +20,11 @@ import org.junit.Test; +import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; @@ -42,18 +42,12 @@ public class StandardWriterResolverTests { private final StandardWriterResolver resolver = new StandardWriterResolver( this.placeholderResolver, "UTF-8", asciidoctor()); - @Test - public void noConfiguredOutputDirectoryAndRelativeInput() { - assertThat(this.resolver.resolveFile("foo", "bar.txt", - new RestDocumentationContext(null, null, null)), is(nullValue())); - } - @Test public void absoluteInput() { String absolutePath = new File("foo").getAbsolutePath(); assertThat( this.resolver.resolveFile(absolutePath, "bar.txt", - new RestDocumentationContext(null, null, null)), + createContext(absolutePath)), is(new File(absolutePath, "bar.txt"))); } @@ -62,7 +56,7 @@ public void configuredOutputAndRelativeInput() { File outputDir = new File("foo").getAbsoluteFile(); assertThat( this.resolver.resolveFile("bar", "baz.txt", - new RestDocumentationContext(null, null, outputDir)), + createContext(outputDir.getAbsolutePath())), is(new File(outputDir, "bar/baz.txt"))); } @@ -72,8 +66,16 @@ public void configuredOutputAndAbsoluteInput() { String absolutePath = new File("bar").getAbsolutePath(); assertThat( this.resolver.resolveFile(absolutePath, "baz.txt", - new RestDocumentationContext(null, null, outputDir)), + createContext(outputDir.getAbsolutePath())), is(new File(absolutePath, "baz.txt"))); } + private RestDocumentationContext createContext(String outputDir) { + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + outputDir); + manualRestDocumentation.beforeTest(getClass(), null); + RestDocumentationContext context = manualRestDocumentation.beforeOperation(); + return context; + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index a9bc7d3cf..86b426a30 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -27,6 +27,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -96,8 +97,7 @@ public Operation build() { new MustacheTemplateEngine( new StandardTemplateResourceResolver(this.templateFormat))); } - RestDocumentationContext context = new RestDocumentationContext(null, null, - this.outputDirectory); + RestDocumentationContext context = createContext(); this.attributes.put(RestDocumentationContext.class.getName(), context); this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( @@ -110,6 +110,14 @@ public Operation build() { this.responseBuilder.buildResponse(), this.attributes); } + private RestDocumentationContext createContext() { + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + this.outputDirectory.getAbsolutePath()); + manualRestDocumentation.beforeTest(null, null); + RestDocumentationContext context = manualRestDocumentation.beforeOperation(); + return context; + } + /** * Basic builder API for creating an {@link OperationRequest}. */ diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java index 1fb3c33bf..d26d9e105 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentation.java @@ -16,7 +16,6 @@ package org.springframework.restdocs.mockmvc; -import org.springframework.restdocs.RestDocumentation; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; @@ -32,7 +31,6 @@ * * @author Andy Wilkinson */ -@SuppressWarnings("deprecation") public abstract class MockMvcRestDocumentation { private static final MockMvcRequestConverter REQUEST_CONVERTER = new MockMvcRequestConverter(); @@ -55,7 +53,7 @@ private MockMvcRestDocumentation() { */ @Deprecated public static MockMvcRestDocumentationConfigurer documentationConfiguration( - RestDocumentation restDocumentation) { + org.springframework.restdocs.RestDocumentation restDocumentation) { return documentationConfiguration( (RestDocumentationContextProvider) restDocumentation); } From 8b039e5f816aa4fa40488baa7cf80ea0bd9bda03 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Apr 2016 17:29:40 +0100 Subject: [PATCH 083/898] Add support for reusing a snippet to document common elements This commit updates all of the Snippet implementations that take one or more descriptors to provide an and method that can be used to create a new Snippet that has additional descriptors. Closes gh-168 --- .../docs/asciidoc/documenting-your-api.adoc | 39 +++++++++++++++++ .../test/java/com/example/SnippetReuse.java | 34 +++++++++++++++ .../example/mockmvc/MockMvcSnippetReuse.java | 43 +++++++++++++++++++ .../restassured/RestAssuredSnippetReuse.java | 43 +++++++++++++++++++ .../restdocs/headers/HeaderDocumentation.java | 11 ++--- .../headers/RequestHeadersSnippet.java | 19 +++++++- .../headers/ResponseHeadersSnippet.java | 19 +++++++- .../restdocs/http/HttpDocumentation.java | 12 +++--- .../hypermedia/HypermediaDocumentation.java | 12 +++--- .../restdocs/hypermedia/LinksSnippet.java | 15 +++++++ .../payload/PayloadDocumentation.java | 12 +++--- .../payload/RequestFieldsSnippet.java | 18 +++++++- .../payload/ResponseFieldsSnippet.java | 18 +++++++- .../request/AbstractParametersSnippet.java | 13 ++++++ .../request/PathParametersSnippet.java | 16 +++++++ .../request/RequestDocumentation.java | 13 +++--- .../request/RequestParametersSnippet.java | 16 +++++++ .../headers/RequestHeadersSnippetTests.java | 23 ++++++++++ .../headers/ResponseHeadersSnippetTests.java | 21 +++++++++ .../hypermedia/LinksSnippetTests.java | 12 ++++++ .../payload/RequestFieldsSnippetTests.java | 16 +++++++ .../payload/ResponseFieldsSnippetTests.java | 21 +++++++++ .../request/PathParametersSnippetTests.java | 13 ++++++ .../RequestParametersSnippetTests.java | 12 ++++++ 24 files changed, 435 insertions(+), 36 deletions(-) create mode 100644 docs/src/test/java/com/example/SnippetReuse.java create mode 100644 docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java create mode 100644 docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index f036275c9..88313f978 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -445,6 +445,45 @@ When documenting HTTP Headers, the test will fail if a documented header is not the request or response. + +[[documenting-your-api-reusing-snippets]] +=== Reusing snippets + +It's common for an API that's being documented to have some features that are common +across several of its resources. To avoid repetition when documenting such resources a +`Snippet` configured with the common elements can be reused. + +First, create the `Snippet` that describes the common elements. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/SnippetReuse.java[tags=field] +---- + +Second, use this snippet and add further descriptors that are resource-specific. For +example: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/MockMvcSnippetReuse.java[tags=use] +---- +<1> Reuse the `pagingLinks` `Snippet` calling `and` to add descriptors that are specific + to the resource that is being documented. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RestAssuredSnippetReuse.java[tags=use] +---- +<1> Reuse the `pagingLinks` `Snippet` calling `and` to add descriptors that are specific + to the resource that is being documented. + +The result of the example is that links with the rels `first`, `last`, `next`, `previous`, +`alpha`, and `bravo` are all documented. + + + [[documenting-your-api-constraints]] === Documenting constraints diff --git a/docs/src/test/java/com/example/SnippetReuse.java b/docs/src/test/java/com/example/SnippetReuse.java new file mode 100644 index 000000000..6ab40ddb6 --- /dev/null +++ b/docs/src/test/java/com/example/SnippetReuse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2016 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 com.example; + +import org.springframework.restdocs.hypermedia.LinksSnippet; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; + +public class SnippetReuse { + + // tag::field[] + protected final LinksSnippet pagingLinks = links( + linkWithRel("first").optional().description("The first page of results"), + linkWithRel("last").optional().description("The last page of results"), + linkWithRel("next").optional().description("The next page of results"), + linkWithRel("prev").optional().description("The previous page of results")); + // end::field[] + +} diff --git a/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java new file mode 100644 index 000000000..34dc3ae1b --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2016 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 com.example.mockmvc; + +import com.example.SnippetReuse; + +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class MockMvcSnippetReuse extends SnippetReuse { + + private MockMvc mockMvc; + + public void documentation() throws Exception { + // tag::use[] + this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("example", this.pagingLinks.and( // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))); + // end::use[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java new file mode 100644 index 000000000..c792a7fbc --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2016 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 com.example.restassured; + +import com.example.SnippetReuse; +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RestAssuredSnippetReuse extends SnippetReuse { + + private RequestSpecification spec; + + public void documentation() throws Exception { + // tag::use[] + RestAssured.given(this.spec) + .accept("application/json") + .filter(document("example", this.pagingLinks.and( // <1> + linkWithRel("alpha").description("Link to the alpha resource"), + linkWithRel("bravo").description("Link to the bravo resource")))) + .get("/").then().assertThat().statusCode(is(200)); + // end::use[] + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java index 85800ecff..1eb4f0f9e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -55,7 +55,7 @@ public static HeaderDescriptor headerWithName(String name) { * @return the snippet that will document the request headers * @see #headerWithName(String) */ - public static Snippet requestHeaders(HeaderDescriptor... descriptors) { + public static RequestHeadersSnippet requestHeaders(HeaderDescriptor... descriptors) { return new RequestHeadersSnippet(Arrays.asList(descriptors)); } @@ -72,7 +72,7 @@ public static Snippet requestHeaders(HeaderDescriptor... descriptors) { * @return the snippet that will document the request headers * @see #headerWithName(String) */ - public static Snippet requestHeaders(Map attributes, + public static RequestHeadersSnippet requestHeaders(Map attributes, HeaderDescriptor... descriptors) { return new RequestHeadersSnippet(Arrays.asList(descriptors), attributes); } @@ -88,7 +88,8 @@ public static Snippet requestHeaders(Map attributes, * @return the snippet that will document the response headers * @see #headerWithName(String) */ - public static Snippet responseHeaders(HeaderDescriptor... descriptors) { + public static ResponseHeadersSnippet responseHeaders( + HeaderDescriptor... descriptors) { return new ResponseHeadersSnippet(Arrays.asList(descriptors)); } @@ -106,7 +107,7 @@ public static Snippet responseHeaders(HeaderDescriptor... descriptors) { * @return the snippet that will document the response headers * @see #headerWithName(String) */ - public static Snippet responseHeaders(Map attributes, + public static ResponseHeadersSnippet responseHeaders(Map attributes, HeaderDescriptor... descriptors) { return new ResponseHeadersSnippet(Arrays.asList(descriptors), attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java index 83dd6b6d5..ecdc933a6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,6 +16,8 @@ package org.springframework.restdocs.headers; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,6 +29,7 @@ * A {@link Snippet} that documents the headers in a request. * * @author Andreas Evers + * @author Andy Wilkinson * @see HeaderDocumentation#requestHeaders(HeaderDescriptor...) * @see HeaderDocumentation#requestHeaders(Map, HeaderDescriptor...) */ @@ -60,4 +63,18 @@ protected Set extractActualHeaders(Operation operation) { return operation.getRequest().getHeaders().keySet(); } + /** + * Returns a new {@code RequestHeadersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(this.getHeaderDescriptors()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new RequestHeadersSnippet(combinedDescriptors, getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java index 979083c42..1a4171679 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -16,6 +16,8 @@ package org.springframework.restdocs.headers; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -27,6 +29,7 @@ * A {@link Snippet} that documents the headers in a response. * * @author Andreas Evers + * @author Andy Wilkinson * @see HeaderDocumentation#responseHeaders(HeaderDescriptor...) * @see HeaderDocumentation#responseHeaders(Map, HeaderDescriptor...) */ @@ -60,4 +63,18 @@ protected Set extractActualHeaders(Operation operation) { return operation.getResponse().getHeaders().keySet(); } + /** + * Returns a new {@code ResponseHeadersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(this.getHeaderDescriptors()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new ResponseHeadersSnippet(combinedDescriptors, getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index f1bc8b389..5d872fe28 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -18,8 +18,6 @@ import java.util.Map; -import org.springframework.restdocs.snippet.Snippet; - /** * Static factory methods for documenting a RESTful API's HTTP requests. * @@ -38,7 +36,7 @@ private HttpDocumentation() { * * @return the snippet that will document the HTTP request */ - public static Snippet httpRequest() { + public static HttpRequestSnippet httpRequest() { return new HttpRequestSnippet(); } @@ -50,7 +48,7 @@ public static Snippet httpRequest() { * @param attributes the attributes * @return the snippet that will document the HTTP request */ - public static Snippet httpRequest(Map attributes) { + public static HttpRequestSnippet httpRequest(Map attributes) { return new HttpRequestSnippet(attributes); } @@ -60,7 +58,7 @@ public static Snippet httpRequest(Map attributes) { * * @return the snippet that will document the HTTP response */ - public static Snippet httpResponse() { + public static HttpResponseSnippet httpResponse() { return new HttpResponseSnippet(); } @@ -72,7 +70,7 @@ public static Snippet httpResponse() { * @param attributes the attributes * @return the snippet that will document the HTTP response */ - public static Snippet httpResponse(Map attributes) { + public static HttpResponseSnippet httpResponse(Map attributes) { return new HttpResponseSnippet(attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index d5ddaf0b4..c2f11b75d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,8 +19,6 @@ import java.util.Arrays; import java.util.Map; -import org.springframework.restdocs.snippet.Snippet; - /** * Static factory methods for documenting a RESTful API that utilizes Hypermedia. * @@ -59,7 +57,7 @@ public static LinkDescriptor linkWithRel(String rel) { * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static Snippet links(LinkDescriptor... descriptors) { + public static LinksSnippet links(LinkDescriptor... descriptors) { return new LinksSnippet(new ContentTypeLinkExtractor(), Arrays.asList(descriptors)); } @@ -83,7 +81,7 @@ public static Snippet links(LinkDescriptor... descriptors) { * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static Snippet links(Map attributes, + public static LinksSnippet links(Map attributes, LinkDescriptor... descriptors) { return new LinksSnippet(new ContentTypeLinkExtractor(), Arrays.asList(descriptors), attributes); @@ -107,7 +105,7 @@ public static Snippet links(Map attributes, * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static Snippet links(LinkExtractor linkExtractor, + public static LinksSnippet links(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); } @@ -132,7 +130,7 @@ public static Snippet links(LinkExtractor linkExtractor, * @param descriptors the descriptions of the response's links * @return the snippet that will document the links */ - public static Snippet links(LinkExtractor linkExtractor, + public static LinksSnippet links(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 771e2e7a8..7c684ba9f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -170,4 +171,18 @@ protected Map createModelForDescriptor(LinkDescriptor descriptor return model; } + /** + * Returns a new {@code RequestHeadersSnippet} configured with this snippet's link + * extractor and attributes, and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public LinksSnippet and(LinkDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(this.descriptorsByRel.values()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new LinksSnippet(this.linkExtractor, combinedDescriptors, getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 097e1c47c..f2391277b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -19,8 +19,6 @@ import java.util.Arrays; import java.util.Map; -import org.springframework.restdocs.snippet.Snippet; - /** * Static factory methods for documenting a RESTful API's request and response payloads. * @@ -117,7 +115,7 @@ public static FieldDescriptor fieldWithPath(String path) { * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static Snippet requestFields(FieldDescriptor... descriptors) { + public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) { return new RequestFieldsSnippet(Arrays.asList(descriptors)); } @@ -142,7 +140,7 @@ public static Snippet requestFields(FieldDescriptor... descriptors) { * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static Snippet requestFields(Map attributes, + public static RequestFieldsSnippet requestFields(Map attributes, FieldDescriptor... descriptors) { return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); } @@ -167,7 +165,7 @@ public static Snippet requestFields(Map attributes, * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static Snippet responseFields(FieldDescriptor... descriptors) { + public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) { return new ResponseFieldsSnippet(Arrays.asList(descriptors)); } @@ -192,7 +190,7 @@ public static Snippet responseFields(FieldDescriptor... descriptors) { * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static Snippet responseFields(Map attributes, + public static ResponseFieldsSnippet responseFields(Map attributes, FieldDescriptor... descriptors) { return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 354a0283e..fdc3e609e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -17,6 +17,8 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -66,4 +68,18 @@ protected byte[] getContent(Operation operation) throws IOException { return operation.getRequest().getContent(); } + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getFieldDescriptors()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 82015ef8f..14d5dd555 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -17,6 +17,8 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -66,4 +68,18 @@ protected byte[] getContent(Operation operation) throws IOException { return operation.getResponse().getContent(); } + /** + * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getFieldDescriptors()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 0be930228..21d900fe8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -122,11 +122,24 @@ protected abstract void verificationFailed(Set undocumentedParameters, * {@link ParameterDescriptor#getName()}. * * @return the map of path descriptors + * @deprecated since 1.1.0 in favor of {@link #getParameterDescriptors()} */ + @Deprecated protected final Map getFieldDescriptors() { return this.descriptorsByName; } + /** + * Returns a {@code Map} of {@link ParameterDescriptor ParameterDescriptors} that will + * be used to generate the documentation key by their + * {@link ParameterDescriptor#getName()}. + * + * @return the map of path descriptors + */ + protected final Map getParameterDescriptors() { + return this.descriptorsByName; + } + /** * Returns a model for the given {@code descriptor}. * diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 77cc1b4d7..01ef25e41 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.request; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -122,4 +124,18 @@ protected void verificationFailed(Set undocumentedParameters, throw new SnippetException(message); } + /** + * Returns a new {@code PathParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public PathParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getParameterDescriptors().values()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new PathParametersSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index e0aa2f4ba..6fa64c9bf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -20,7 +20,6 @@ import java.util.Map; import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.snippet.Snippet; /** * Static factory methods for documenting aspects of a request sent to a RESTful API. @@ -61,7 +60,8 @@ public static ParameterDescriptor parameterWithName(String name) { * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static Snippet pathParameters(ParameterDescriptor... descriptors) { + public static PathParametersSnippet pathParameters( + ParameterDescriptor... descriptors) { return new PathParametersSnippet(Arrays.asList(descriptors)); } @@ -84,7 +84,7 @@ public static Snippet pathParameters(ParameterDescriptor... descriptors) { * @param descriptors the descriptions of the parameters in the request's path * @return the snippet that will document the parameters */ - public static Snippet pathParameters(Map attributes, + public static PathParametersSnippet pathParameters(Map attributes, ParameterDescriptor... descriptors) { return new PathParametersSnippet(Arrays.asList(descriptors), attributes); } @@ -107,7 +107,8 @@ public static Snippet pathParameters(Map attributes, * @return the snippet * @see OperationRequest#getParameters() */ - public static Snippet requestParameters(ParameterDescriptor... descriptors) { + public static RequestParametersSnippet requestParameters( + ParameterDescriptor... descriptors) { return new RequestParametersSnippet(Arrays.asList(descriptors)); } @@ -131,8 +132,8 @@ public static Snippet requestParameters(ParameterDescriptor... descriptors) { * @return the snippet that will document the parameters * @see OperationRequest#getParameters() */ - public static Snippet requestParameters(Map attributes, - ParameterDescriptor... descriptors) { + public static RequestParametersSnippet requestParameters( + Map attributes, ParameterDescriptor... descriptors) { return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index 5b1f9ba81..e85423fc1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.request; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; @@ -84,4 +86,18 @@ protected Set extractActualParameters(Operation operation) { return operation.getRequest().getParameters().keySet(); } + /** + * Returns a new {@code RequestParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getParameterDescriptors().values()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new RequestParametersSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index fb13925a4..594415628 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -143,4 +143,27 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { .header("Accept", "*/*").build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectRequestHeaders("additional-descriptors") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") + .row("Accept", "two").row("Accept-Encoding", "three") + .row("Accept-Language", "four").row("Cache-Control", "five") + .row("Connection", "six")); + HeaderDocumentation + .requestHeaders(headerWithName("X-Test").description("one"), + headerWithName("Accept").description("two"), + headerWithName("Accept-Encoding").description("three"), + headerWithName("Accept-Language").description("four")) + .and(headerWithName("Cache-Control").description("five"), + headerWithName("Connection").description("six")) + .document(operationBuilder("additional-descriptors") + .request("http://localhost").header("X-Test", "test") + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 6e16402ba..d3f2ffbd5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -133,4 +133,25 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { .header("Etag", "lskjadldj3ii32l2ij23").build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectResponseHeaders("additional-descriptors") + .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") + .row("Content-Type", "two").row("Etag", "three") + .row("Cache-Control", "five").row("Vary", "six")); + HeaderDocumentation + .responseHeaders(headerWithName("X-Test").description("one"), + headerWithName("Content-Type").description("two"), + headerWithName("Etag").description("three")) + .and(headerWithName("Cache-Control").description("five"), + headerWithName("Vary") + .description("six")) + .document(operationBuilder("additional-descriptors").response() + .header("X-Test", "test") + .header("Content-Type", "application/json") + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0").header("Vary", "User-Agent") + .build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index b04109765..b78b3b678 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -132,4 +132,16 @@ public void linksWithCustomDescriptorAttributes() throws IOException { .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectLinks("additional-descriptors") + .withContents(tableWithHeader("Relation", "Description").row("a", "one") + .row("b", "two")); + HypermediaDocumentation + .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), + new LinkDescriptor("a").description("one")) + .and(new LinkDescriptor("b").description("two")) + .document(operationBuilder("additional-descriptors").build()); + } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 948383671..195ed58a8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -160,4 +160,20 @@ public void xmlRequestFields() throws IOException { .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectRequestFields("additional-descriptors") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a.b", "Number", "one").row("a.c", "String", "two") + .row("a", "Object", "three")); + + PayloadDocumentation + .requestFields(fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two")) + .and(fieldWithPath("a").description("three")) + .document(operationBuilder("additional-descriptors") + .request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 4d996699b..8e33fadad 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -211,4 +211,25 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectResponseFields("additional-descriptors") + .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")); + PayloadDocumentation + .responseFields(fieldWithPath("id").description("one"), + fieldWithPath("date").description("two"), + fieldWithPath("assets").description("three")) + .and(fieldWithPath("assets[]").description("four"), + fieldWithPath("assets[].id").description("five"), + fieldWithPath("assets[].name").description("six")) + .document(operationBuilder("additional-descriptors").response() + .content("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + + " [{\"id\":356,\"name\": \"sample\"}]}") + .build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 1b50d3d17..fb28e1cd5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -133,6 +133,19 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectPathParameters("additional-descriptors").withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") + .row("a", "one").row("b", "two")); + RequestDocumentation.pathParameters(parameterWithName("a").description("one")) + .and(parameterWithName("b").description("two")) + .document(operationBuilder("additional-descriptors") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") + .build()); + } + private String getTitle() { return this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}/{b}" : "`/{a}/{b}`"; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 204806522..4e533fd97 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -128,4 +128,16 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException .build()); } + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectRequestParameters("additional-descriptors") + .withContents(tableWithHeader("Parameter", "Description").row("a", "one") + .row("b", "two")); + RequestDocumentation.requestParameters(parameterWithName("a").description("one")) + .and(parameterWithName("b").description("two")) + .document(operationBuilder("additional-descriptors") + .request("http://localhost").param("a", "bravo") + .param("b", "bravo").build()); + } + } From bab779ac919f98b668cf0e79f5f2cbb6b33ae64a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Apr 2016 17:50:56 +0100 Subject: [PATCH 084/898] Disable wrapping in Asciidoctor HTTP request and response snippets The HTTP request and response snippets are intended to be an exact representation of the HTTP request or response which would not wrap. This commit disables wrapping by adding the nowrap option to the default templates for these two snippets. Closes gh-204 --- .../restdocs/templates/asciidoctor/default-http-request.snippet | 2 +- .../templates/asciidoctor/default-http-response.snippet | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet index a3e529aed..9893e2a03 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-request.snippet @@ -1,4 +1,4 @@ -[source,http] +[source,http,options="nowrap"] ---- {{method}} {{path}} HTTP/1.1 {{#headers}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet index fe095d36e..90a25e851 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-http-response.snippet @@ -1,4 +1,4 @@ -[source,http] +[source,http,options="nowrap"] ---- HTTP/1.1 {{statusCode}} {{statusReason}} {{#headers}} From 2da1a0a979f824cf78539bf3075fc5608c07c44d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 12 Apr 2016 19:36:34 +0100 Subject: [PATCH 085/898] Update tests following nowrap changes made in bab779a Closes gh-204 --- .../restdocs/test/SnippetMatchers.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index bb0c146cd..34d84a5eb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -72,7 +72,7 @@ public static HttpRequestMatcher httpRequest(TemplateFormat format, RequestMethod requestMethod, String uri) { if ("adoc".equals(format.getFileExtension())) { return new HttpRequestMatcher(requestMethod, uri, - new AsciidoctorCodeBlockMatcher<>("http"), 3); + new AsciidoctorCodeBlockMatcher<>("http", "nowrap"), 3); } return new HttpRequestMatcher(requestMethod, uri, new MarkdownCodeBlockMatcher<>("http"), 2); @@ -82,7 +82,7 @@ public static HttpResponseMatcher httpResponse(TemplateFormat format, HttpStatus status) { if ("adoc".equals(format.getFileExtension())) { return new HttpResponseMatcher(status, - new AsciidoctorCodeBlockMatcher<>("http"), 3); + new AsciidoctorCodeBlockMatcher<>("http", "nowrap"), 3); } return new HttpResponseMatcher(status, new MarkdownCodeBlockMatcher<>("http"), 2); } @@ -90,7 +90,7 @@ public static HttpResponseMatcher httpResponse(TemplateFormat format, @SuppressWarnings({ "rawtypes" }) public static CodeBlockMatcher codeBlock(TemplateFormat format, String language) { if ("adoc".equals(format.getFileExtension())) { - return new AsciidoctorCodeBlockMatcher(language); + return new AsciidoctorCodeBlockMatcher(language, null); } return new MarkdownCodeBlockMatcher(language); } @@ -180,9 +180,10 @@ public T content(String content) { public static class AsciidoctorCodeBlockMatcher> extends CodeBlockMatcher { - protected AsciidoctorCodeBlockMatcher(String language) { + protected AsciidoctorCodeBlockMatcher(String language, String options) { super(TemplateFormats.asciidoctor()); - this.addLine("[source," + language + "]"); + this.addLine("[source," + language + + (options == null ? "" : ",options=\"" + options + "\"") + "]"); this.addLine("----"); this.addLine("----"); } From e5aa5ba7283329f759faaf057d18e4fcafcd6e67 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 10:07:41 +0100 Subject: [PATCH 086/898] Make it easier to write a preprocessor that only changes req or resp The commit introduces OperationPreprocessorAdapter, an abstract implementation of OperationProcessor that returns to request and response as-is. OperationPreprocessorAdapter is intended to be subclassed by OperationProcessor implementations that only modify the request or the response. Implementations the modify both should continue to implement OperationPreprocess directly. Closes gh-154 --- .../OperationPreprocessorAdapter.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java new file mode 100644 index 000000000..f2654c1bf --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2016 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.operation.preprocess; + +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationResponse; + +/** + * An implementation of {@link OperationPreprocessor} that returns the request and + * response as-is. To be subclasses by preprocessor implementations that only modify the + * request or the response. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public abstract class OperationPreprocessorAdapter implements OperationPreprocessor { + + /** + * Returns the given {@code request} as-is. + * + * @param request the request + * @return the unmodified request + */ + @Override + public OperationRequest preprocess(OperationRequest request) { + return request; + } + + /** + * Returns the given {@code response} as-is. + * + * @param response the response + * @return the unmodified response + */ + @Override + public OperationResponse preprocess(OperationResponse response) { + return response; + } + +} From ecdc1971c29e05fc2afbdbb6cc2d8ab43793bf49 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 12:00:56 +0100 Subject: [PATCH 087/898] Add default descriptions for Hibernate Validator's constraints Closes gh-151 --- .../docs/asciidoc/documenting-your-api.adoc | 15 + .../DefaultConstraintDescriptions.properties | 14 +- ...dleConstraintDescriptionResolverTests.java | 263 ++++++++++++++---- 3 files changed, 233 insertions(+), 59 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 88313f978..3e409f6e5 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -534,6 +534,21 @@ Default descriptions are provided for all of the Bean Validation 1.1's constrain * Pattern * Size +Default descriptions are also provided for constraints from Hibernate Validator: + +* CreditCardNumber +* EAN +* Email +* Length +* LuhnCheck +* Mod10Check +* Mod11Check +* NotBlank +* NotEmpty +* Range +* SafeHtml +* URL + To override the default descriptions, or to provide a new description, create a resource bundle with the base name `org.springframework.restdocs.constraints.ConstraintDescriptions`. The Spring diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties index c945d4865..1cd5320f8 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties @@ -10,4 +10,16 @@ javax.validation.constraints.NotNull.description=Must not be null javax.validation.constraints.Null.description=Must be null javax.validation.constraints.Past.description=Must be in the past javax.validation.constraints.Pattern.description=Must match the regular expression '${regexp}' -javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive \ No newline at end of file +javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive +org.hibernate.validator.constraints.CreditCardNumber.description=Must be a well-formed credit card number +org.hibernate.validator.constraints.EAN.description=Must be a well-formed ${type} number +org.hibernate.validator.constraints.Email.description=Must be a well-formed email address +org.hibernate.validator.constraints.Length.description=Length must be between ${min} and ${max} inclusive +org.hibernate.validator.constraints.LuhnCheck.description=Must pass the Luhn Modulo 10 checksum algorithm +org.hibernate.validator.constraints.Mod10Check.description=Must pass the Mod10 checksum algorithm +org.hibernate.validator.constraints.Mod11Check.description=Must pass the Mod11 checksum algorithm +org.hibernate.validator.constraints.NotBlank.description=Must not be blank +org.hibernate.validator.constraints.NotEmpty.description=Must not be empty +org.hibernate.validator.constraints.Range.description=Must be at least ${min} and at most ${max} +org.hibernate.validator.constraints.SafeHtml.description=Must be safe HTML +org.hibernate.validator.constraints.URL.description=Must be a well-formed URL \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index 06334f51e..f9a7f734e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -16,11 +16,13 @@ package org.springframework.restdocs.constraints; +import java.lang.annotation.Annotation; +import java.math.BigDecimal; import java.net.URL; import java.util.Collections; -import java.util.HashMap; +import java.util.Date; +import java.util.List; import java.util.ListResourceBundle; -import java.util.Map; import java.util.ResourceBundle; import javax.validation.constraints.AssertFalse; @@ -37,8 +39,23 @@ import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; +import org.hibernate.validator.constraints.CreditCardNumber; +import org.hibernate.validator.constraints.EAN; +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.LuhnCheck; +import org.hibernate.validator.constraints.Mod10Check; +import org.hibernate.validator.constraints.Mod11Check; +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.NotEmpty; +import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.SafeHtml; import org.junit.Test; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @@ -54,111 +71,151 @@ public class ResourceBundleConstraintDescriptionResolverTests { @Test public void defaultMessageAssertFalse() { - String description = this.resolver.resolveDescription(new Constraint( - AssertFalse.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be false"))); + assertThat(constraintDescriptionForField("assertFalse"), + is(equalTo("Must be false"))); } @Test public void defaultMessageAssertTrue() { - String description = this.resolver.resolveDescription(new Constraint( - AssertTrue.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be true"))); + assertThat(constraintDescriptionForField("assertTrue"), + is(equalTo("Must be true"))); } @Test public void defaultMessageDecimalMax() { - Map configuration = new HashMap<>(); - configuration.put("value", "9.875"); - String description = this.resolver.resolveDescription( - new Constraint(DecimalMax.class.getName(), configuration)); - assertThat(description, is(equalTo("Must be at most 9.875"))); + assertThat(constraintDescriptionForField("decimalMax"), + is(equalTo("Must be at most 9.875"))); } @Test public void defaultMessageDecimalMin() { - Map configuration = new HashMap<>(); - configuration.put("value", "1.5"); - String description = this.resolver.resolveDescription( - new Constraint(DecimalMin.class.getName(), configuration)); - assertThat(description, is(equalTo("Must be at least 1.5"))); + assertThat(constraintDescriptionForField("decimalMin"), + is(equalTo("Must be at least 1.5"))); } @Test public void defaultMessageDigits() { - Map configuration = new HashMap<>(); - configuration.put("integer", "2"); - configuration.put("fraction", "5"); - String description = this.resolver.resolveDescription( - new Constraint(Digits.class.getName(), configuration)); - assertThat(description, is(equalTo( - "Must have at most 2 integral digits and 5 " + "fractional digits"))); + assertThat(constraintDescriptionForField("digits"), is( + equalTo("Must have at most 2 integral digits and 5 fractional digits"))); } @Test public void defaultMessageFuture() { - String description = this.resolver.resolveDescription(new Constraint( - Future.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be in the future"))); + assertThat(constraintDescriptionForField("future"), + is(equalTo("Must be in the future"))); } @Test public void defaultMessageMax() { - Map configuration = new HashMap<>(); - configuration.put("value", 10); - String description = this.resolver - .resolveDescription(new Constraint(Max.class.getName(), configuration)); - assertThat(description, is(equalTo("Must be at most 10"))); + assertThat(constraintDescriptionForField("max"), + is(equalTo("Must be at most 10"))); } @Test public void defaultMessageMin() { - Map configuration = new HashMap<>(); - configuration.put("value", 10); - String description = this.resolver - .resolveDescription(new Constraint(Min.class.getName(), configuration)); - assertThat(description, is(equalTo("Must be at least 10"))); + assertThat(constraintDescriptionForField("min"), + is(equalTo("Must be at least 10"))); } @Test public void defaultMessageNotNull() { - String description = this.resolver.resolveDescription(new Constraint( - NotNull.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must not be null"))); + assertThat(constraintDescriptionForField("notNull"), + is(equalTo("Must not be null"))); } @Test public void defaultMessageNull() { - String description = this.resolver.resolveDescription(new Constraint( - Null.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be null"))); + assertThat(constraintDescriptionForField("nul"), is(equalTo("Must be null"))); } @Test public void defaultMessagePast() { - String description = this.resolver.resolveDescription(new Constraint( - Past.class.getName(), Collections.emptyMap())); - assertThat(description, is(equalTo("Must be in the past"))); + assertThat(constraintDescriptionForField("past"), + is(equalTo("Must be in the past"))); } @Test public void defaultMessagePattern() { - Map configuration = new HashMap<>(); - configuration.put("regexp", "[A-Z][a-z]+"); - String description = this.resolver.resolveDescription( - new Constraint(Pattern.class.getName(), configuration)); - assertThat(description, + assertThat(constraintDescriptionForField("pattern"), is(equalTo("Must match the regular expression '[A-Z][a-z]+'"))); } @Test public void defaultMessageSize() { - Map configuration = new HashMap<>(); - configuration.put("min", 2); - configuration.put("max", 10); - String description = this.resolver - .resolveDescription(new Constraint(Size.class.getName(), configuration)); - assertThat(description, is(equalTo("Size must be between 2 and 10 inclusive"))); + assertThat(constraintDescriptionForField("size"), + is(equalTo("Size must be between 2 and 10 inclusive"))); + } + + @Test + public void defaultMessageCreditCardNumber() { + assertThat(constraintDescriptionForField("creditCardNumber"), + is(equalTo("Must be a well-formed credit card number"))); + } + + @Test + public void defaultMessageEan() { + assertThat(constraintDescriptionForField("ean"), + is(equalTo("Must be a well-formed EAN13 number"))); + } + + @Test + public void defaultMessageEmail() { + assertThat(constraintDescriptionForField("email"), + is(equalTo("Must be a well-formed email address"))); + } + + @Test + public void defaultMessageLength() { + assertThat(constraintDescriptionForField("length"), + is(equalTo("Length must be between 2 and 10 inclusive"))); + } + + @Test + public void defaultMessageLuhnCheck() { + assertThat(constraintDescriptionForField("luhnCheck"), + is(equalTo("Must pass the Luhn Modulo 10 checksum algorithm"))); + } + + @Test + public void defaultMessageMod10Check() { + assertThat(constraintDescriptionForField("mod10Check"), + is(equalTo("Must pass the Mod10 checksum algorithm"))); + } + + @Test + public void defaultMessageMod11Check() { + assertThat(constraintDescriptionForField("mod11Check"), + is(equalTo("Must pass the Mod11 checksum algorithm"))); + } + + @Test + public void defaultMessageNotBlank() { + assertThat(constraintDescriptionForField("notBlank"), + is(equalTo("Must not be blank"))); + } + + @Test + public void defaultMessageNotEmpty() { + assertThat(constraintDescriptionForField("notEmpty"), + is(equalTo("Must not be empty"))); + } + + @Test + public void defaultMessageRange() { + assertThat(constraintDescriptionForField("range"), + is(equalTo("Must be at least 10 and at most 100"))); + } + + @Test + public void defaultMessageSafeHtml() { + assertThat(constraintDescriptionForField("safeHtml"), + is(equalTo("Must be safe HTML"))); + } + + @Test + public void defaultMessageUrl() { + assertThat(constraintDescriptionForField("url"), + is(equalTo("Must be a well-formed URL"))); } @Test @@ -206,4 +263,94 @@ protected Object[][] getContents() { assertThat(description, is(equalTo("Not null"))); } + private String constraintDescriptionForField(String name) { + return this.resolver.resolveDescription(getConstraintFromField(name)); + } + + private Constraint getConstraintFromField(String name) { + Annotation[] annotations = ReflectionUtils.findField(Constrained.class, name) + .getAnnotations(); + Assert.isTrue(annotations.length == 1); + return new Constraint(annotations[0].annotationType().getName(), + AnnotationUtils.getAnnotationAttributes(annotations[0])); + } + + private static class Constrained { + + @AssertFalse + private boolean assertFalse; + + @AssertTrue + private boolean assertTrue; + + @DecimalMax("9.875") + private BigDecimal decimalMax; + + @DecimalMin("1.5") + private BigDecimal decimalMin; + + @Digits(integer = 2, fraction = 5) + private String digits; + + @Future + private Date future; + + @Max(10) + private int max; + + @Min(10) + private int min; + + @NotNull + private String notNull; + + @Null + private String nul; + + @Past + private Date past; + + @Pattern(regexp = "[A-Z][a-z]+") + private String pattern; + + @Size(min = 2, max = 10) + private List size; + + @CreditCardNumber + private String creditCardNumber; + + @EAN + private String ean; + + @Email + private String email; + + @Length(min = 2, max = 10) + private String length; + + @LuhnCheck + private String luhnCheck; + + @Mod10Check + private String mod10Check; + + @Mod11Check + private String mod11Check; + + @NotBlank + private String notBlank; + + @NotEmpty + private String notEmpty; + + @Range(min = 10, max = 100) + private int range; + + @SafeHtml + private String safeHtml; + + @org.hibernate.validator.constraints.URL + private String url; + } + } From ae53a4e8ebf6f5c1dd2d2715a7a66e5e96dcf41e Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 12:36:50 +0100 Subject: [PATCH 088/898] Allow request and path parameters to be marked as optional Closes gh-169 --- .../docs/asciidoc/documenting-your-api.adoc | 5 ++-- .../request/AbstractParametersSnippet.java | 8 +++++- .../restdocs/request/ParameterDescriptor.java | 25 ++++++++++++++++++- .../request/PathParametersSnippetTests.java | 18 ++++++++++++- .../RequestParametersSnippetTests.java | 13 ++++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 3e409f6e5..9550aaf50 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -348,7 +348,8 @@ table describing the parameters that are supported by the resource. When documenting request parameters, the test will fail if an undocumented request parameter is used in the request. Similarly, the test will also fail if a documented -request parameter is not found in the request. +request parameter is not found in the request and the request parameter has not been +marked as optional. If you do not want to document a request parameter, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described @@ -396,7 +397,7 @@ built using one of the methods on `RestDocumentationRequestBuilders` rather than When documenting path parameters, the test will fail if an undocumented path parameter is used in the request. Similarly, the test will also fail if a documented path parameter -is not found in the request. +is not found in the request and the path parameter has not been marked as optional. If you do not want to document a path parameter, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 21d900fe8..b132aa08d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -85,7 +85,13 @@ protected Map createModel(Operation operation) { private void verifyParameterDescriptors(Operation operation) { Set actualParameters = extractActualParameters(operation); - Set expectedParameters = this.descriptorsByName.keySet(); + Set expectedParameters = new HashSet<>(); + for (Entry entry : this.descriptorsByName + .entrySet()) { + if (!entry.getValue().isOptional()) { + expectedParameters.add(entry.getKey()); + } + } Set undocumentedParameters = new HashSet<>(actualParameters); undocumentedParameters.removeAll(expectedParameters); Set missingParameters = new HashSet<>(expectedParameters); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index 9172a5525..d19eab4f3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -29,6 +29,8 @@ public class ParameterDescriptor extends IgnorableDescriptor Date: Wed, 13 Apr 2016 14:09:08 +0100 Subject: [PATCH 089/898] Fix HalLinkExtractor's extraction of link hrefs Closes gh-220 --- .../restdocs/hypermedia/HalLinkExtractor.java | 17 ++++++++++------- .../hypermedia/LinkExtractorsPayloadTests.java | 4 ++-- .../link-payloads/atom/wrong-format.json | 8 ++++++-- .../hal/multiple-links-different-rels.json | 8 ++++++-- .../hal/multiple-links-same-rels.json | 6 +++++- .../link-payloads/hal/single-link.json | 4 +++- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java index c3f28d733..eb55843aa 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -54,9 +54,9 @@ private static List convertToLinks(Object object, String rel) { List links = new ArrayList<>(); if (object instanceof Collection) { @SuppressWarnings("unchecked") - Collection hrefObjects = (Collection) object; - for (Object hrefObject : hrefObjects) { - maybeAddLink(maybeCreateLink(rel, hrefObject), links); + Collection possibleLinkObjects = (Collection) object; + for (Object possibleLinkObject : possibleLinkObjects) { + maybeAddLink(maybeCreateLink(rel, possibleLinkObject), links); } } else { @@ -65,9 +65,12 @@ private static List convertToLinks(Object object, String rel) { return links; } - private static Link maybeCreateLink(String rel, Object possibleHref) { - if (possibleHref instanceof String) { - return new Link(rel, (String) possibleHref); + private static Link maybeCreateLink(String rel, Object possibleLinkObject) { + if (possibleLinkObject instanceof Map) { + Object hrefObject = ((Map) possibleLinkObject).get("href"); + if (hrefObject instanceof String) { + return new Link(rel, (String) hrefObject); + } } return null; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index 7353bbeae..b26374e81 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -53,7 +53,7 @@ public class LinkExtractorsPayloadTests { private final String linkType; - @Parameters + @Parameters(name = "{1}") public static Collection data() { return Arrays.asList(new Object[] { new HalLinkExtractor(), "hal" }, new Object[] { new AtomLinkExtractor(), "atom" }); diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json index 04a6d84b8..a5e2e40fd 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/wrong-format.json @@ -1,5 +1,9 @@ { - "_links": { - "alpha": ["http://alpha.example.com/one", "http://alpha.example.com/two"] + "links": { + "alpha": [{ + "href": "http://alpha.example.com/one" + }, { + "href": "http://alpha.example.com/two" + }] } } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json index 80d36d72a..08d0f3eaf 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json @@ -1,6 +1,10 @@ { "_links": { - "alpha": "http://alpha.example.com", - "bravo": "http://bravo.example.com" + "alpha": { + "href": "http://alpha.example.com" + }, + "bravo": { + "href": "http://bravo.example.com" + } } } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json index 04a6d84b8..45ebca3ca 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json @@ -1,5 +1,9 @@ { "_links": { - "alpha": ["http://alpha.example.com/one", "http://alpha.example.com/two"] + "alpha": [{ + "href": "http://alpha.example.com/one" + }, { + "href": "http://alpha.example.com/two" + }] } } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json index be90b3779..d19ca125c 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json @@ -1,5 +1,7 @@ { "_links": { - "alpha": "http://alpha.example.com" + "alpha": { + "href": "http://alpha.example.com" + } } } \ No newline at end of file From c4b74387080bd71e1aaf6acba4bf150ffd3e2dd8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 14:01:33 +0100 Subject: [PATCH 090/898] Derive a link's description from its title Previously, a LinkDescriptor had to be created with both a rel and a description. If a description was not provided a failure would occur. This commit relaxes the above-described restriction by allowing a link's title to be used as its default description. If a descriptor has a description, it will always be used irrespective of whether or not the link has a title. If the descriptor does not have a description and the link does have a title, the link's title will be used. If the descriptor does not have a description and the link does not have a title a failure will occur. Closes gh-105 --- .../docs/asciidoc/documenting-your-api.adoc | 4 ++ .../hypermedia/AtomLinkExtractor.java | 4 +- .../restdocs/hypermedia/HalLinkExtractor.java | 7 ++- .../hypermedia/HypermediaDocumentation.java | 4 ++ .../restdocs/hypermedia/Link.java | 36 ++++++++++++++- .../restdocs/hypermedia/LinksSnippet.java | 45 +++++++++++++++---- .../LinkExtractorsPayloadTests.java | 8 ++-- .../hypermedia/LinksSnippetFailureTests.java | 12 +++++ .../hypermedia/LinksSnippetTests.java | 14 ++++++ .../atom/multiple-links-different-rels.json | 3 +- .../atom/multiple-links-same-rels.json | 3 +- .../link-payloads/atom/single-link.json | 3 +- .../hal/multiple-links-different-rels.json | 3 +- .../hal/multiple-links-same-rels.json | 3 +- .../link-payloads/hal/single-link.json | 3 +- ...kMvcRestDocumentationIntegrationTests.java | 6 ++- ...uredRestDocumentationIntegrationTests.java | 6 ++- 17 files changed, 137 insertions(+), 27 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 9550aaf50..2dee3919d 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -38,6 +38,10 @@ include::{examples-dir}/com/example/restassured/Hypermedia.java[tag=links] The result is a snippet named `links.adoc` that contains a table describing the resource's links. +TIP: If a link in the response has a `title`, the description can be omitted from its +descriptor and the `title` will be used. If you omit the description and the link does +not have a `title` a failure will occur. + When documenting links, the test will fail if an undocumented link is found in the response. Similarly, the test will also fail if a documented link is not found in the response and the link has not been marked as optional. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java index ad12497a3..71a2d8dc7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/AtomLinkExtractor.java @@ -51,7 +51,9 @@ private static Link maybeCreateLink(Map 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); + Object titleObject = linkMap.get("title"); + return new Link((String) relObject, (String) hrefObject, + titleObject instanceof String ? (String) titleObject : null); } return null; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java index eb55843aa..866a8322f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HalLinkExtractor.java @@ -67,9 +67,12 @@ private static List convertToLinks(Object object, String rel) { private static Link maybeCreateLink(String rel, Object possibleLinkObject) { if (possibleLinkObject instanceof Map) { - Object hrefObject = ((Map) possibleLinkObject).get("href"); + Map possibleLinkMap = (Map) possibleLinkObject; + Object hrefObject = possibleLinkMap.get("href"); if (hrefObject instanceof String) { - return new Link(rel, (String) hrefObject); + Object titleObject = possibleLinkMap.get("title"); + return new Link(rel, (String) hrefObject, + titleObject instanceof String ? (String) titleObject : null); } } return null; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index c2f11b75d..f54931283 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -53,6 +53,10 @@ public static LinkDescriptor linkWithRel(String rel) { * If you do not want to document a link, a link descriptor can be marked as * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param descriptors the descriptions of the response's links * @return the snippet that will document the links diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java index 112886f0b..01d5165ce 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -29,6 +29,8 @@ public class Link { private final String href; + private final String title; + /** * Creates a new {@code Link} with the given {@code rel} and {@code href}. * @@ -36,8 +38,21 @@ public class Link { * @param href The link's href */ public Link(String rel, String href) { + this(rel, href, null); + } + + /** + * Creates a new {@code Link} with the given {@code rel}, {@code href}, and + * {@code title}. + * + * @param rel The link's rel + * @param href The link's href + * @param title The link's title + */ + public Link(String rel, String href, String title) { this.rel = rel; this.href = href; + this.title = title; } /** @@ -56,12 +71,21 @@ public String getHref() { return this.href; } + /** + * Returns the link's {@code title}, or {@code null} if it does not have a title. + * @return the link's {@code title} or {@code null} + */ + public String getTitle() { + return this.title; + } + @Override public int hashCode() { - int prime = 31; + final int prime = 31; int result = 1; result = prime * result + this.href.hashCode(); result = prime * result + this.rel.hashCode(); + result = prime * result + ((this.title == null) ? 0 : this.title.hashCode()); return result; } @@ -83,13 +107,21 @@ public boolean equals(Object obj) { if (!this.rel.equals(other.rel)) { return false; } + if (this.title == null) { + if (other.title != null) { + return false; + } + } + else if (!this.title.equals(other.title)) { + return false; + } return true; } @Override public String toString() { return new ToStringCreator(this).append("rel", this.rel).append("href", this.href) - .toString(); + .append("title", this.title).toString(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 7c684ba9f..528b444a5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -77,12 +77,6 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel"); - if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), - "The descriptor for link '" + descriptor.getRel() - + "' must either have a description or be" + " marked as " - + "ignored"); - } this.descriptorsByRel.put(descriptor.getRel(), descriptor); } } @@ -90,14 +84,16 @@ protected LinksSnippet(LinkExtractor linkExtractor, List descrip @Override protected Map createModel(Operation operation) { OperationResponse response = operation.getResponse(); + Map> links; try { - validate(this.linkExtractor.extractLinks(response)); + links = this.linkExtractor.extractLinks(response); + validate(links); } catch (IOException ex) { throw new ModelCreationException(ex); } Map model = new HashMap<>(); - model.put("links", createLinksModel()); + model.put("links", createLinksModel(links)); return model; } @@ -135,17 +131,48 @@ private void validate(Map> links) { } } - private List> createLinksModel() { + private List> createLinksModel(Map> links) { List> model = new ArrayList<>(); for (Entry entry : this.descriptorsByRel.entrySet()) { LinkDescriptor descriptor = entry.getValue(); if (!descriptor.isIgnored()) { + if (descriptor.getDescription() == null) { + descriptor = createDescriptor( + getDescriptionFromLinkTitle(links, descriptor.getRel()), + descriptor); + } model.add(createModelForDescriptor(descriptor)); } } return model; } + private String getDescriptionFromLinkTitle(Map> links, + String rel) { + List linksForRel = links.get(rel); + if (linksForRel != null) { + for (Link link : linksForRel) { + if (link.getTitle() != null) { + return link.getTitle(); + } + } + } + throw new SnippetException("No description was provided for the link with rel '" + + rel + "' and no title was available from the link in the payload"); + } + + private LinkDescriptor createDescriptor(String description, LinkDescriptor source) { + LinkDescriptor newDescriptor = new LinkDescriptor(source.getRel()) + .description(description); + if (source.isOptional()) { + newDescriptor.optional(); + } + if (source.isIgnored()) { + newDescriptor.ignored(); + } + return newDescriptor; + } + /** * Returns a {@code Map} of {@link LinkDescriptor LinkDescriptors} keyed by their * {@link LinkDescriptor#getRel() rels}. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java index b26374e81..fa5781645 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinkExtractorsPayloadTests.java @@ -68,14 +68,15 @@ public LinkExtractorsPayloadTests(LinkExtractor linkExtractor, String linkType) public void singleLink() throws IOException { Map> links = this.linkExtractor .extractLinks(createResponse("single-link")); - assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com")), links); + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com", "Alpha")), + links); } @Test public void multipleLinksWithDifferentRels() throws IOException { Map> links = this.linkExtractor .extractLinks(createResponse("multiple-links-different-rels")); - assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com"), + assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com", "Alpha"), new Link("bravo", "http://bravo.example.com")), links); } @@ -83,7 +84,8 @@ public void multipleLinksWithDifferentRels() throws IOException { public void multipleLinksWithSameRels() throws IOException { Map> links = this.linkExtractor .extractLinks(createResponse("multiple-links-same-rels")); - assertLinks(Arrays.asList(new Link("alpha", "http://alpha.example.com/one"), + assertLinks(Arrays.asList( + new Link("alpha", "http://alpha.example.com/one", "Alpha one"), new Link("alpha", "http://alpha.example.com/two")), links); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index 6ef2dfd8a..217b753e5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -79,4 +79,16 @@ public void undocumentedLinkAndMissingLink() throws IOException { this.snippet.getOutputDirectory()).build()); } + @Test + public void linkWithNoDescription() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + equalTo("No description was provided for the link with rel 'foo' and no" + + " title was available from the link in the payload")); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), + Arrays.asList(new LinkDescriptor("foo"))) + .document(new OperationBuilder("link-with-no-description", + this.snippet.getOutputDirectory()).build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index b78b3b678..be122de6f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -87,6 +87,20 @@ public void documentedLinks() throws IOException { .document(operationBuilder("documented-links").build()); } + @Test + public void linkDescriptionFromTitleInPayload() throws IOException { + this.snippet.expectLinks("link-description-from-title-in-payload") + .withContents(tableWithHeader("Relation", "Description").row("a", "one") + .row("b", "Link b")); + new LinksSnippet( + new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), + new Link("b", "bravo", "Link b")), + Arrays.asList(new LinkDescriptor("a").description("one"), + new LinkDescriptor("b"))).document( + operationBuilder("link-description-from-title-in-payload") + .build()); + } + @Test public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json index 35e63537d..081c5d98d 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-different-rels.json @@ -1,7 +1,8 @@ { "links": [ { "rel": "alpha", - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" }, { "rel": "bravo", "href": "http://bravo.example.com" diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json index ff0d3f4ae..28d76ac7d 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/multiple-links-same-rels.json @@ -1,7 +1,8 @@ { "links": [ { "rel": "alpha", - "href": "http://alpha.example.com/one" + "href": "http://alpha.example.com/one", + "title": "Alpha one" }, { "rel": "alpha", "href": "http://alpha.example.com/two" diff --git a/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json index 57532675b..d6339c031 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/atom/single-link.json @@ -1,6 +1,7 @@ { "links": [ { "rel": "alpha", - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" } ] } \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json index 08d0f3eaf..6eecbf5c6 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-different-rels.json @@ -1,7 +1,8 @@ { "_links": { "alpha": { - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" }, "bravo": { "href": "http://bravo.example.com" diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json index 45ebca3ca..9e5f51dcd 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/multiple-links-same-rels.json @@ -1,7 +1,8 @@ { "_links": { "alpha": [{ - "href": "http://alpha.example.com/one" + "href": "http://alpha.example.com/one", + "title": "Alpha one" }, { "href": "http://alpha.example.com/two" }] diff --git a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json index d19ca125c..43bf4273c 100644 --- a/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json +++ b/spring-restdocs-core/src/test/resources/link-payloads/hal/single-link.json @@ -1,7 +1,8 @@ { "_links": { "alpha": { - "href": "http://alpha.example.com" + "href": "http://alpha.example.com", + "title": "Alpha" } } } \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 51c35ee4d..54bce09ff 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -38,7 +38,6 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.hypermedia.Link; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -461,7 +460,10 @@ private static class TestController { public ResponseEntity> foo() { Map response = new HashMap<>(); response.put("a", "alpha"); - response.put("links", Arrays.asList(new Link("rel", "href"))); + Map link = new HashMap<>(); + link.put("rel", "rel"); + link.put("href", "href"); + response.put("links", Arrays.asList(link)); HttpHeaders headers = new HttpHeaders(); headers.add("a", "alpha"); return new ResponseEntity<>(response, headers, HttpStatus.OK); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 8d4da39a6..408d29059 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -40,7 +40,6 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.hypermedia.Link; import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -329,7 +328,10 @@ static class TestApplication { public ResponseEntity> foo() { Map response = new HashMap<>(); response.put("a", "alpha"); - response.put("links", Arrays.asList(new Link("rel", "href"))); + Map link = new HashMap<>(); + link.put("rel", "rel"); + link.put("href", "href"); + response.put("links", Arrays.asList(link)); HttpHeaders headers = new HttpHeaders(); headers.add("a", "alpha"); headers.add("Foo", "http://localhost:12345/foo/bar"); From 2897b8d1b4d2d6923b6376c37dc58b94a7f7d3f2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 13 Apr 2016 17:50:19 +0100 Subject: [PATCH 091/898] =?UTF-8?q?Provide=20a=20preprocessor=20for=20modi?= =?UTF-8?q?fying=20a=20request=E2=80=99s=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes gh-155 --- .../customizing-requests-and-responses.adoc | 9 + .../operation/OperationRequestFactory.java | 20 +- ...ametersModifyingOperationPreprocessor.java | 181 ++++++++++++++++++ .../operation/preprocess/Preprocessors.java | 13 +- ...rsModifyingOperationPreprocessorTests.java | 136 +++++++++++++ 5 files changed, 356 insertions(+), 3 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 8cb020544..04a1b48e1 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -109,6 +109,7 @@ the name is equal to any of the given header names. response where the name matches any of the given regular expression patterns. + [[customizing-requests-and-responses-preprocessors-replace-patterns]] ==== Replacing patterns @@ -118,6 +119,14 @@ replaced. +[[customizing-requests-and-responses-preprocessors-modify-request-parameters]] +==== Modifying request parameters + +`modifyParameters` on `Preprocessors` can be used to add, set, and remove request +parameters. + + + [[customizing-requests-and-responses-preprocessors-modify-uris]] ==== Modifying URIs diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index c105fd75c..961987b5b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -72,7 +72,7 @@ public OperationRequest createFrom(OperationRequest original, byte[] newContent) * @param original The original request * @param newHeaders The new headers * - * @return The new request with the new content + * @return The new request with the new headers */ public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { @@ -81,6 +81,22 @@ public OperationRequest createFrom(OperationRequest original, original.getParts()); } + /** + * Creates a new {@code OperationRequest} based on the given {@code original} but with + * the given {@code newParameters}. + * + * @param original The original request + * @param newParameters The new parameters + * + * @return The new request with the new parameters + */ + public OperationRequest createFrom(OperationRequest original, + Parameters newParameters) { + return new StandardOperationRequest(original.getUri(), original.getMethod(), + original.getContent(), original.getHeaders(), newParameters, + original.getParts()); + } + private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, byte[] content) { return new HttpHeadersHelper(originalHeaders) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java new file mode 100644 index 000000000..5006c73e3 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java @@ -0,0 +1,181 @@ +/* + * Copyright 2012-2016 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.operation.preprocess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.Parameters; +import org.springframework.util.Assert; + +/** + * An {@link OperationPreprocessor} that can be used to modify a request's + * {@link OperationRequest#getParameters()} by adding, setting, and removing parameters. + * + * @author Andy Wilkinson + * @since 1.1.0 + */ +public final class ParametersModifyingOperationPreprocessor + extends OperationPreprocessorAdapter { + + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final List modifications = new ArrayList<>(); + + @Override + public OperationRequest preprocess(OperationRequest request) { + Parameters parameters = new Parameters(); + parameters.putAll(request.getParameters()); + for (Modification modification : this.modifications) { + modification.apply(parameters); + } + return this.requestFactory.createFrom(request, parameters); + } + + /** + * Adds a parameter with the given {@code name} and {@code value}. + * + * @param name the name + * @param value the value + * @return {@code this} + */ + public ParametersModifyingOperationPreprocessor add(String name, String value) { + this.modifications.add(new AddParameterModification(name, value)); + return this; + } + + /** + * Sets the parameter with the given {@code name} to have the given {@code values}. + * + * @param name the name + * @param values the values + * @return {@code this} + */ + public ParametersModifyingOperationPreprocessor set(String name, String... values) { + Assert.notEmpty(values, "At least one value must be provided"); + this.modifications.add(new SetParameterModification(name, Arrays.asList(values))); + return this; + } + + /** + * Removes the parameter with the given {@code name}. + * + * @param name the name of the parameter + * @return {@code this} + */ + public ParametersModifyingOperationPreprocessor remove(String name) { + this.modifications.add(new RemoveParameterModification(name)); + return this; + } + + /** + * Removes the given {@code value} from the parameter with the given {@code name}. + * + * @param name the name + * @param value the value + * @return {@code this} + */ + public ParametersModifyingOperationPreprocessor remove(String name, String value) { + this.modifications.add(new RemoveValueParameterModification(name, value)); + return this; + } + + private interface Modification { + + void apply(Parameters parameters); + + } + + private static final class AddParameterModification implements Modification { + + private final String name; + + private final String value; + + private AddParameterModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void apply(Parameters parameters) { + parameters.add(this.name, this.value); + } + + } + + private static final class SetParameterModification implements Modification { + + private final String name; + + private final List values; + + private SetParameterModification(String name, List values) { + this.name = name; + this.values = values; + } + + @Override + public void apply(Parameters parameters) { + parameters.put(this.name, this.values); + } + + } + + private static final class RemoveParameterModification implements Modification { + + private final String name; + + private RemoveParameterModification(String name) { + this.name = name; + } + + @Override + public void apply(Parameters parameters) { + parameters.remove(this.name); + } + + } + + private static final class RemoveValueParameterModification implements Modification { + + private final String name; + + private final String value; + + private RemoveValueParameterModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void apply(Parameters parameters) { + List values = parameters.get(this.name); + if (values != null) { + values.remove(this.value); + if (values.isEmpty()) { + parameters.remove(this.name); + } + } + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index d600407aa..23a310378 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -127,7 +127,7 @@ public static OperationPreprocessor maskLinks(String mask) { /** * Returns an {@code OperationPreprocessor} that will modify the content of the - * request or response by replacing occurences of the given {@code pattern} with the + * request or response by replacing occurrences of the given {@code pattern} with the * given {@code replacement}. * * @param pattern the pattern @@ -140,4 +140,15 @@ public static OperationPreprocessor replacePattern(Pattern pattern, new PatternReplacingContentModifier(pattern, replacement)); } + /** + * Returns a {@code ParametersModifyingOperationPreprocessor} that can then be + * configured to modify the parameters of the request. + * + * @return the preprocessor + * @since 1.1.0 + */ + public static ParametersModifyingOperationPreprocessor modifyParameters() { + return new ParametersModifyingOperationPreprocessor(); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java new file mode 100644 index 000000000..4d8035f39 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2016 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.operation.preprocess; + +import java.net.URI; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link ParametersModifyingOperationPreprocessor}. + * + * @author Andy Wilkinson + */ +public class ParametersModifyingOperationPreprocessorTests { + + private final ParametersModifyingOperationPreprocessor preprocessor = new ParametersModifyingOperationPreprocessor(); + + @Test + public void addNewParameter() { + Parameters parameters = new Parameters(); + assertThat(this.preprocessor.add("a", "alpha") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha"))); + } + + @Test + public void addValueToExistingParameter() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + assertThat( + this.preprocessor.add("a", "alpha").preprocess(createRequest(parameters)) + .getParameters(), + hasEntry(equalTo("a"), contains("apple", "alpha"))); + } + + @Test + public void setNewParameter() { + Parameters parameters = new Parameters(); + assertThat( + this.preprocessor.set("a", "alpha", "avocado") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha", "avocado"))); + } + + @Test + public void setExistingParameter() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + assertThat( + this.preprocessor.set("a", "alpha", "avocado") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha", "avocado"))); + } + + @Test + public void removeNonExistentParameter() { + Parameters parameters = new Parameters(); + assertThat(this.preprocessor.remove("a").preprocess(createRequest(parameters)) + .getParameters().size(), is(equalTo(0))); + } + + @Test + public void removeParameter() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + assertThat( + this.preprocessor.set("a", "alpha", "avocado") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha", "avocado"))); + } + + @Test + public void removeParameterValueForNonExistentParameter() { + Parameters parameters = new Parameters(); + assertThat( + this.preprocessor.remove("a", "apple") + .preprocess(createRequest(parameters)).getParameters().size(), + is(equalTo(0))); + } + + @Test + public void removeParameterValueWithMultipleValues() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + parameters.add("a", "alpha"); + assertThat( + this.preprocessor.remove("a", "apple") + .preprocess(createRequest(parameters)).getParameters(), + hasEntry(equalTo("a"), contains("alpha"))); + } + + @Test + public void removeParameterValueWithSingleValueRemovesEntryEntirely() { + Parameters parameters = new Parameters(); + parameters.add("a", "apple"); + assertThat( + this.preprocessor.remove("a", "apple") + .preprocess(createRequest(parameters)).getParameters().size(), + is(equalTo(0))); + } + + private OperationRequest createRequest(Parameters parameters) { + return new OperationRequestFactory().create(URI.create("http://localhost:8080"), + HttpMethod.GET, new byte[0], new HttpHeaders(), parameters, + Collections.emptyList()); + } + +} From b62c5f0374e109fe0b9fee8a63ca34572127f006 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Apr 2016 10:30:52 +0100 Subject: [PATCH 092/898] Make it easier to document common portions of req and resp payloads This commit adds a new andWithPrefix(String, FieldDescriptor[]) method to both RequestFieldsSnippet and ResponseFieldsSnippet. It can be used to add descriptors to an existing snippet, applying the given prefix to the additional descriptors as it does so. This allows the descriptors for a portion of a payload to be created once and then reused, irrespective of where in the payload the portion appears. Closes gh-221 --- .../docs/asciidoc/documenting-your-api.adoc | 79 +++++++++++++++++++ docs/src/test/java/com/example/Payload.java | 34 ++++++++ .../java/com/example/mockmvc/Payload.java | 21 +++++ .../java/com/example/restassured/Payload.java | 24 ++++++ .../payload/AbstractFieldsSnippet.java | 26 ++++++ .../payload/RequestFieldsSnippet.java | 19 ++++- .../payload/ResponseFieldsSnippet.java | 19 ++++- .../payload/RequestFieldsSnippetTests.java | 15 ++++ .../payload/ResponseFieldsSnippetTests.java | 14 ++++ 9 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 docs/src/test/java/com/example/Payload.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 2dee3919d..f798b34a5 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -296,6 +296,85 @@ method will be used in the documentation. +[[documenting-your-api-request-response-payloads-reusing-field-descriptors]] +==== Reusing field descriptors + +In addition to the general support for <>, the request and response snippets allow additional descriptors to be +configured with a path prefix. This allows the descriptors for a repeated portion of a +request or response payload to be created once and then reused. + +Consider an endpoint that returns a book: + +[source,json,indent=0] +---- + { + "title": "Pride and Prejudice", + "author": "Jane Austen" + } +---- + +The paths for `title` and `author` are simply `title` and `author` respectively. + +Now consider an endpoint that returns an array of books: + +[source,json,indent=0] +---- + [{ + "title": "Pride and Prejudice", + "author": "Jane Austen" + }, + { + "title": "To Kill a Mockingbird", + "author": "Harper Lee" + }] +---- + +The paths for `title` and `author` are `[].title` and `[].author` respectively. The only +difference between the single book and the array of books is that the fields' paths now +have a `[].` prefix. + +The descriptors that document a book can be created: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=book-descriptors] +---- + +They can then be used to document a single book: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=single-book] +---- +<1> Document `title` and `author` using existing descriptors + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=single-book] +---- +<1> Document `title` and `author` using existing descriptors + +And an array of books: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=book-array] +---- +<1> Document the array +<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] +---- +<1> Document the array +<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` + [[documenting-your-api-request-parameters]] === Request parameters diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java new file mode 100644 index 000000000..d33de4d16 --- /dev/null +++ b/docs/src/test/java/com/example/Payload.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2016 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 com.example; + +import org.springframework.restdocs.payload.FieldDescriptor; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +public class Payload { + + @SuppressWarnings("unused") + public void bookFieldDescriptors() { + // tag::book-descriptors[] + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + // end::book-descriptors[] + } + +} diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 4424b1f31..b811c1a6c 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -27,6 +27,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; @@ -70,4 +71,24 @@ public void constraints() throws Exception { // end::constraints[] } + public void descriptorReuse() throws Exception { + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + + // tag::single-book[] + this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("book", responseFields(book))); // <1> + // end::single-book[] + + // tag::book-array[] + this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("book", responseFields( + fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))); // <2> + // end::book-array[] + } + } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index ee6c2456f..225eb6194 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -16,6 +16,7 @@ package com.example.restassured; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import com.jayway.restassured.RestAssured; @@ -72,4 +73,27 @@ public void constraints() throws Exception { .then().assertThat().statusCode(is(200)); } + public void descriptorReuse() throws Exception { + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + + // tag::single-book[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("book", responseFields(book))) // <1> + .when().get("/books/1") + .then().assertThat().statusCode(is(200)); + // end::single-book[] + + // tag::book-array[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("books", responseFields( + fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))) // <2> + .when().get("/books/1") + .then().assertThat().statusCode(is(200)); + // end::book-array[] + + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index dcff13bdd..19322b281 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -180,4 +180,30 @@ protected Map createModelForDescriptor(FieldDescriptor descripto return model; } + /** + * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} + * applied to their paths. + * + * @param pathPrefix the path prefix + * @param descriptors the descriptors to copy + * @return the copied descriptors with the prefix applied + */ + protected final List applyPathPrefix(String pathPrefix, + List descriptors) { + List prefixedDescriptors = new ArrayList<>(); + for (FieldDescriptor descriptor : descriptors) { + FieldDescriptor prefixedDescriptor = new FieldDescriptor( + pathPrefix + descriptor.getPath()) + .description(descriptor.getDescription()) + .type(descriptor.getType()); + if (descriptor.isIgnored()) { + prefixedDescriptor.ignored(); + } + if (descriptor.isOptional()) { + prefixedDescriptor.optional(); + } + prefixedDescriptors.add(prefixedDescriptor); + } + return prefixedDescriptors; + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index fdc3e609e..9e2968873 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -72,13 +72,30 @@ protected byte[] getContent(Operation operation) throws IOException { * Returns a new {@code RequestFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. + * * @param additionalDescriptors the additional descriptors * @return the new snippet */ public RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestFieldsSnippet andWithPrefix(String pathPrefix, + FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + combinedDescriptors.addAll( + applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 14d5dd555..28815c4ce 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -72,13 +72,30 @@ protected byte[] getContent(Operation operation) throws IOException { * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. + * * @param additionalDescriptors the additional descriptors * @return the new snippet */ public ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public ResponseFieldsSnippet andWithPrefix(String pathPrefix, + FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + combinedDescriptors.addAll( + applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 195ed58a8..79e02a8c6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -176,4 +176,19 @@ public void additionalDescriptors() throws IOException { .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } + @Test + public void prefixedAdditionalDescriptors() throws IOException { + this.snippet.expectRequestFields("prefixed-additional-descriptors") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "Object", "one").row("a.b", "Number", "two") + .row("a.c", "String", "three")); + + PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), + fieldWithPath("c").description("three")) + .document(operationBuilder("prefixed-additional-descriptors") + .request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 8e33fadad..ae490e50d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -232,4 +232,18 @@ public void additionalDescriptors() throws IOException { .build()); } + @Test + public void prefixedAdditionalDescriptors() throws IOException { + this.snippet.expectResponseFields("prefixed-additional-descriptors") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "Object", "one").row("a.b", "Number", "two") + .row("a.c", "String", "three")); + + PayloadDocumentation.responseFields(fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), + fieldWithPath("c").description("three")) + .document(operationBuilder("prefixed-additional-descriptors").response() + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + } + } From 0e52ef04a1a378e03a3b27cb4163fcba8de933ba Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Apr 2016 11:18:27 +0100 Subject: [PATCH 093/898] Document how to ignore common links without repetition Closes gh-177 --- .../docs/asciidoc/documenting-your-api.adoc | 16 +++++++++ .../src/test/java/com/example/Hypermedia.java | 34 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 docs/src/test/java/com/example/Hypermedia.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index f798b34a5..9242fcbc3 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -84,6 +84,22 @@ If your API represents its links in a format other than Atom or HAL, you can pro own implementation of the `LinkExtractor` interface to extract the links from the response. + + +[[documenting-your-api-hypermedia-ignoring-common-links]] +==== Ignoring common links + +Rather than documenting links that are common to every response, such as `_self` and +`curies` when using HAL, you may want to document them once in an overview section and +then ignore them in the rest of your API's documentation. To do so, you can build on the +<> to add link +descriptors to a snippet that's preconfigured to ignore certain links. For example: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links] +---- + [[documenting-your-api-request-response-payloads]] === Request and response payloads diff --git a/docs/src/test/java/com/example/Hypermedia.java b/docs/src/test/java/com/example/Hypermedia.java new file mode 100644 index 000000000..10f168684 --- /dev/null +++ b/docs/src/test/java/com/example/Hypermedia.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2016 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 com.example; + +import org.springframework.restdocs.hypermedia.HypermediaDocumentation; +import org.springframework.restdocs.hypermedia.LinkDescriptor; +import org.springframework.restdocs.hypermedia.LinksSnippet; + +import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; + +public class Hypermedia { + + // tag::ignore-links[] + public static LinksSnippet links(LinkDescriptor... descriptors) { + return HypermediaDocumentation.links(linkWithRel("_self").ignored().optional(), + linkWithRel("curies").ignored()).and(descriptors); + } + // end::ignore-links[] + +} From 043e796502d7b283da15b6da46305976c7e671dc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Apr 2016 12:41:05 +0100 Subject: [PATCH 094/898] =?UTF-8?q?Introduce=20relaxed=20snippets=20that?= =?UTF-8?q?=20don=E2=80=99t=20fail=20when=20item=20is=20not=20documented?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, many of the snippets would fail when something wasn’t documented. This worked well when writing exhaustive API documentation, but was cumbersome when trying to document a scenario that might only being interested in a subset of the links, response fields, etc. It was necessary to mark things that were not of interest as being ignored. This commit introduces a relaxed variant of several snippets. A relaxed snippet will not fail if something has not been documented. Instead, the undocumented thing will be ignored. If something has been documented but it does not exist a failure will still occur. Closes gh-175 --- .../hypermedia/HypermediaDocumentation.java | 100 ++++++++++++++++++ .../restdocs/hypermedia/LinksSnippet.java | 50 ++++++++- .../payload/AbstractFieldsSnippet.java | 32 +++++- .../payload/PayloadDocumentation.java | 69 ++++++++++++ .../payload/RequestFieldsSnippet.java | 39 ++++++- .../payload/ResponseFieldsSnippet.java | 40 ++++++- .../request/AbstractParametersSnippet.java | 38 ++++++- .../request/PathParametersSnippet.java | 42 +++++++- .../request/RequestDocumentation.java | 72 ++++++++++++- .../request/RequestParametersSnippet.java | 45 +++++++- .../hypermedia/LinksSnippetTests.java | 11 ++ .../payload/RequestFieldsSnippetTests.java | 13 +++ .../payload/ResponseFieldsSnippetTests.java | 12 +++ .../request/PathParametersSnippetTests.java | 13 +++ .../RequestParametersSnippetTests.java | 11 ++ 15 files changed, 559 insertions(+), 28 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index f54931283..95f70b3be 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -66,6 +66,26 @@ public static LinksSnippet links(LinkDescriptor... descriptors) { Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response automatically based on its + * content type and will be documented using the given {@code descriptors}. + *

          + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkDescriptor... descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), + Arrays.asList(descriptors), true); + } + /** * Returns a new {@code Snippet} that will document the links in the API call's * response. The given {@code attributes} will be available during snippet generation. @@ -80,6 +100,10 @@ public static LinksSnippet links(LinkDescriptor... descriptors) { * If you do not want to document a link, a link descriptor can be marked as * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param attributes the attributes * @param descriptors the descriptions of the response's links @@ -91,6 +115,29 @@ public static LinksSnippet links(Map attributes, Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document 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 + * and will be documented using the given {@code descriptors}. + *

          + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(Map attributes, + LinkDescriptor... descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), + Arrays.asList(descriptors), attributes, true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. Links will be extracted from the response using the given @@ -104,6 +151,10 @@ public static LinksSnippet links(Map attributes, * If you do not want to document a link, a link descriptor can be marked as * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param linkExtractor used to extract the links from the response * @param descriptors the descriptions of the response's links @@ -114,6 +165,27 @@ public static LinksSnippet links(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response using the given + * {@code linkExtractor} and will be documented using the given {@code descriptors}. + *

          + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + LinkDescriptor... descriptors) { + return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. The given {@code attributes} will be available during snippet generation. @@ -128,6 +200,10 @@ public static LinksSnippet links(LinkExtractor linkExtractor, * If you do not want to document a link, a link descriptor can be marked as * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the * generated snippet while avoiding the failure described above. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. * * @param attributes the attributes * @param linkExtractor used to extract the links from the response @@ -139,6 +215,30 @@ public static LinksSnippet links(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response using the given {@code linkExtractor} and + * will be documented using the given {@code descriptors}. + *

          + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + Map attributes, LinkDescriptor... descriptors) { + return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes, + true); + } + /** * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia * Application Language (HAL) format where the links are found in a map named diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 528b444a5..51b28f927 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -50,22 +51,41 @@ public class LinksSnippet extends TemplatedSnippet { private final LinkExtractor linkExtractor; + private final boolean ignoreUndocumentedLinks; + /** * Creates a new {@code LinksSnippet} that will extract links using the given * {@code linkExtractor} and document them using the given {@code descriptors}. + * Undocumented links will trigger a failure. * * @param linkExtractor the link extractor * @param descriptors the link descriptors */ protected LinksSnippet(LinkExtractor linkExtractor, List descriptors) { - this(linkExtractor, descriptors, null); + this(linkExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code LinksSnippet} that will extract links using the given + * {@code linkExtractor} and document them using the given {@code descriptors}. If + * {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be ignored + * and will not trigger a failure. + * + * @param linkExtractor the link extractor + * @param descriptors the link descriptors + * @param ignoreUndocumentedLinks whether undocumented links should be ignored + */ + protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, + boolean ignoreUndocumentedLinks) { + this(linkExtractor, descriptors, null, ignoreUndocumentedLinks); } /** * Creates a new {@code LinksSnippet} that will extract links using the given * {@code linkExtractor} and document them using the given {@code descriptors}. The * given {@code attributes} will be included in the model during template rendering. + * Undocumented links will trigger a failure. * * @param linkExtractor the link extractor * @param descriptors the link descriptors @@ -73,12 +93,30 @@ protected LinksSnippet(LinkExtractor linkExtractor, */ protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, Map attributes) { + this(linkExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code LinksSnippet} that will extract links using the given + * {@code linkExtractor} and document them using the given {@code descriptors}. The + * given {@code attributes} will be included in the model during template rendering. + * If {@code ignoreUndocumentedLinks} is {@code true}, undocumented links will be + * ignored and will not trigger a failure. + * + * @param linkExtractor the link extractor + * @param descriptors the link descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedLinks whether undocumented links should be ignored + */ + protected LinksSnippet(LinkExtractor linkExtractor, List descriptors, + Map attributes, boolean ignoreUndocumentedLinks) { super("links", attributes); this.linkExtractor = linkExtractor; for (LinkDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getRel(), "Link descriptors must have a rel"); this.descriptorsByRel.put(descriptor.getRel(), descriptor); } + this.ignoreUndocumentedLinks = ignoreUndocumentedLinks; } @Override @@ -100,8 +138,14 @@ protected Map createModel(Operation operation) { private void validate(Map> links) { Set actualRels = links.keySet(); - Set undocumentedRels = new HashSet<>(actualRels); - undocumentedRels.removeAll(this.descriptorsByRel.keySet()); + Set undocumentedRels; + if (this.ignoreUndocumentedLinks) { + undocumentedRels = Collections.emptySet(); + } + else { + undocumentedRels = new HashSet<>(actualRels); + undocumentedRels.removeAll(this.descriptorsByRel.keySet()); + } Set requiredRels = new HashSet<>(); for (Entry relAndDescriptor : this.descriptorsByRel diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 19322b281..c64507128 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -39,20 +39,42 @@ */ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { - private List fieldDescriptors; + private final List fieldDescriptors; + + private final boolean ignoreUndocumentedFields; /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields}. The fields will be documented using the given * {@code descriptors} and the given {@code attributes} will be included in the model - * during template rendering. + * during template rendering. Undocumented fields will trigger a failure. * * @param type the type of the fields * @param descriptors the field descriptors * @param attributes the additional attributes + * @deprecated since 1.1 in favor of + * {@link #AbstractFieldsSnippet(String, List, Map, boolean)} */ + @Deprecated protected AbstractFieldsSnippet(String type, List descriptors, Map attributes) { + this(type, descriptors, attributes, false); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be included in the model + * during template rendering. If {@code ignoreUndocumentedFields} is {@code true}, + * undocumented fields will be ignored and will not trigger a failure. + * + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected AbstractFieldsSnippet(String type, List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { super(type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); @@ -65,6 +87,7 @@ protected AbstractFieldsSnippet(String type, List descriptors, } this.fieldDescriptors = descriptors; + this.ignoreUndocumentedFields = ignoreUndocumentedFields; } @Override @@ -111,8 +134,9 @@ private ContentHandler getContentHandler(Operation operation) { private void validateFieldDocumentation(ContentHandler payloadHandler) { List missingFields = payloadHandler .findMissingFields(this.fieldDescriptors); - String undocumentedPayload = payloadHandler - .getUndocumentedContent(this.fieldDescriptors); + + String undocumentedPayload = this.ignoreUndocumentedFields ? null + : payloadHandler.getUndocumentedContent(this.fieldDescriptors); if (!missingFields.isEmpty() || StringUtils.hasText(undocumentedPayload)) { String message = ""; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index f2391277b..bb94c960d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -119,6 +119,22 @@ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) return new RequestFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operations's + * request payload. The fields will be documented using the given {@code descriptors}. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + FieldDescriptor... descriptors) { + return new RequestFieldsSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * request payload. The fields will be documented using the given {@code descriptors} @@ -145,6 +161,24 @@ public static RequestFieldsSnippet requestFields(Map attributes, return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * request payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + Map attributes, FieldDescriptor... descriptors) { + return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -169,6 +203,23 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor return new ResponseFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + FieldDescriptor... descriptors) { + return new ResponseFieldsSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -195,4 +246,22 @@ public static ResponseFieldsSnippet responseFields(Map attribute return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + Map attributes, FieldDescriptor... descriptors) { + return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes, true); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 9e2968873..cfb2ee265 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -37,25 +37,56 @@ public class RequestFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code RequestFieldsSnippet} that will document the fields in the - * request using the given {@code descriptors}. + * request using the given {@code descriptors}. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors */ protected RequestFieldsSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. If {@code ignoreUndocumentedFields} is + * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestFieldsSnippet(List descriptors, + boolean ignoreUndocumentedFields) { + this(descriptors, null, ignoreUndocumentedFields); } /** * Creates a new {@code RequestFieldsSnippet} that will document the fields in the * request using the given {@code descriptors}. The given {@code attributes} will be - * included in the model during template rendering. + * included in the model during template rendering. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors * @param attributes the additional attributes */ protected RequestFieldsSnippet(List descriptors, Map attributes) { - super("request", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestFieldsSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + super("request", descriptors, attributes, ignoreUndocumentedFields); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 28815c4ce..06b5ac655 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -37,25 +37,57 @@ public class ResponseFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the - * response using the given {@code descriptors}. + * response using the given {@code descriptors}. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors */ protected ResponseFieldsSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * response using the given {@code descriptors}. If {@code ignoreUndocumentedFields} + * is {@code true}, undocumented fields will be ignored and will not trigger a + * failure. + * + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected ResponseFieldsSnippet(List descriptors, + boolean ignoreUndocumentedFields) { + this(descriptors, null, ignoreUndocumentedFields); } /** * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the * response using the given {@code descriptors}. The given {@code attributes} will be - * included in the model during template rendering. + * included in the model during template rendering. Undocumented fields will trigger a + * failure. * * @param descriptors the descriptors * @param attributes the additional attributes */ protected ResponseFieldsSnippet(List descriptors, Map attributes) { - super("response", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * response using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected ResponseFieldsSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + super("response", descriptors, attributes, ignoreUndocumentedFields); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index b132aa08d..9d97737d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.request; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -40,18 +41,42 @@ public abstract class AbstractParametersSnippet extends TemplatedSnippet { private final Map descriptorsByName = new LinkedHashMap<>(); + private final boolean ignoreUndocumentedParameters; + /** * Creates a new {@code AbstractParametersSnippet} that will produce a snippet with * the given {@code snippetName} that will document parameters using the given * {@code descriptors}. The given {@code attributes} will be included in the model - * during template rendering. + * during template rendering. Undocumented parameters will trigger a failure. * * @param snippetName The snippet name * @param descriptors The descriptors * @param attributes The additional attributes + * @deprecated since 1.1 in favour of + * {@link #AbstractParametersSnippet(String, List, Map, boolean)} */ + @Deprecated protected AbstractParametersSnippet(String snippetName, List descriptors, Map attributes) { + this(snippetName, descriptors, attributes, false); + } + + /** + * Creates a new {@code AbstractParametersSnippet} that will produce a snippet with + * the given {@code snippetName} that will document parameters using the given + * {@code descriptors}. The given {@code attributes} will be included in the model + * during template rendering. If {@code ignoreUndocumentedParameters} is {@code true}, + * undocumented parameters will be ignored and will not trigger a failure. + * + * @param snippetName The snippet name + * @param descriptors The descriptors + * @param attributes The additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected AbstractParametersSnippet(String snippetName, + List descriptors, Map attributes, + boolean ignoreUndocumentedParameters) { super(snippetName, attributes); for (ParameterDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getName(), @@ -64,6 +89,7 @@ protected AbstractParametersSnippet(String snippetName, } this.descriptorsByName.put(descriptor.getName(), descriptor); } + this.ignoreUndocumentedParameters = ignoreUndocumentedParameters; } @Override @@ -92,8 +118,14 @@ private void verifyParameterDescriptors(Operation operation) { expectedParameters.add(entry.getKey()); } } - Set undocumentedParameters = new HashSet<>(actualParameters); - undocumentedParameters.removeAll(expectedParameters); + Set undocumentedParameters; + if (this.ignoreUndocumentedParameters) { + undocumentedParameters = Collections.emptySet(); + } + else { + undocumentedParameters = new HashSet<>(actualParameters); + undocumentedParameters.removeAll(expectedParameters); + } Set missingParameters = new HashSet<>(expectedParameters); missingParameters.removeAll(actualParameters); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 01ef25e41..509a604b7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -44,25 +44,59 @@ public class PathParametersSnippet extends AbstractParametersSnippet { /** * Creates a new {@code PathParametersSnippet} that will document the request's path - * parameters using the given {@code descriptors}. + * parameters using the given {@code descriptors}. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors */ protected PathParametersSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code PathParametersSnippet} that will document the request's path + * parameters using the given {@code descriptors}. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected PathParametersSnippet(List descriptors, + boolean ignoreUndocumentedParameters) { + this(descriptors, null, ignoreUndocumentedParameters); } /** * Creates a new {@code PathParametersSnippet} that will document the request's path * parameters using the given {@code descriptors}. The given {@code attributes} will - * be included in the model during template rendering. + * be included in the model during template rendering. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ protected PathParametersSnippet(List descriptors, Map attributes) { - super("path-parameters", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code PathParametersSnippet} that will document the request's path + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected PathParametersSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedParameters) { + super("path-parameters", descriptors, attributes, ignoreUndocumentedParameters); } @Override diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 6fa64c9bf..4c4aee73d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -65,6 +65,22 @@ public static PathParametersSnippet pathParameters( return new PathParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

          + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + ParameterDescriptor... descriptors) { + return new PathParametersSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -89,6 +105,24 @@ public static PathParametersSnippet pathParameters(Map attribute return new PathParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

          + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + Map attributes, ParameterDescriptor... descriptors) { + return new PathParametersSnippet(Arrays.asList(descriptors), attributes, true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The parameters will be documented using the given @@ -112,6 +146,23 @@ public static RequestParametersSnippet requestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

          + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors The descriptions of the request's parameters + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + ParameterDescriptor... descriptors) { + return new RequestParametersSnippet(Arrays.asList(descriptors), true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -137,4 +188,23 @@ public static RequestParametersSnippet requestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

          + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parameters + * @return the snippet that will document the parameters + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + Map attributes, ParameterDescriptor... descriptors) { + return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index e85423fc1..37a5696d6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -42,25 +42,60 @@ public class RequestParametersSnippet extends AbstractParametersSnippet { /** * Creates a new {@code RequestParametersSnippet} that will document the request's - * parameters using the given {@code descriptors}. + * parameters using the given {@code descriptors}. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors */ protected RequestParametersSnippet(List descriptors) { - this(descriptors, null); + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestParametersSnippet} that will document the request's + * parameters using the given {@code descriptors}. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected RequestParametersSnippet(List descriptors, + boolean ignoreUndocumentedParameters) { + this(descriptors, null, ignoreUndocumentedParameters); } /** * Creates a new {@code RequestParametersSnippet} that will document the request's * parameters using the given {@code descriptors}. The given {@code attributes} will - * be included in the model during template rendering. + * be included in the model during template rendering. Undocumented parameters will + * trigger a failure. * * @param descriptors the parameter descriptors * @param attributes the additional attributes */ protected RequestParametersSnippet(List descriptors, Map attributes) { - super("request-parameters", descriptors, attributes); + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestParametersSnippet} that will document the request's + * parameters using the given {@code descriptors}. The given {@code attributes} will + * be included in the model during template rendering. If + * {@code ignoreUndocumentedParameters} is {@code true}, undocumented parameters will + * be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParameters whether undocumented parameters should be + * ignored + */ + protected RequestParametersSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedParameters) { + super("request-parameters", descriptors, attributes, + ignoreUndocumentedParameters); } @Override diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index be122de6f..e94abfc35 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -56,6 +56,17 @@ public void ignoredLink() throws IOException { .document(operationBuilder("ignored-link").build()); } + @Test + public void allUndocumentedLinksCanBeIgnored() throws IOException { + this.snippet.expectLinks("ignore-all-undocumented").withContents( + tableWithHeader("Relation", "Description").row("b", "Link b")); + new LinksSnippet( + new StubLinkExtractor().withLinks(new Link("a", "alpha"), + new Link("b", "bravo")), + Arrays.asList(new LinkDescriptor("b").description("Link b")), true) + .document(operationBuilder("ignore-all-undocumented").build()); + } + @Test public void documentedOptionalLink() throws IOException { this.snippet.expectLinks("documented-optional-link").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 79e02a8c6..d94a9071c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -93,6 +93,19 @@ public void ignoredRequestField() throws IOException { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { + this.snippet.expectRequestFields("ignore-all-undocumented") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), + true).document( + operationBuilder("ignore-all-undocumented") + .request("http://localhost") + .content("{\"a\": 5, \"b\": 4}").build()); + } + @Test public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index ae490e50d..215eb4c87 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -105,6 +105,18 @@ public void ignoredResponseField() throws IOException { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void allUndocumentedFieldsCanBeIgnored() throws IOException { + this.snippet.expectResponseFields("ignore-all-undocumented") + .withContents(tableWithHeader("Path", "Type", "Description").row("b", + "Number", "Field b")); + + new ResponseFieldsSnippet( + Arrays.asList(fieldWithPath("b").description("Field b")), true) + .document(operationBuilder("ignore-all-undocumented").response() + .content("{\"a\": 5, \"b\": 4}").build()); + } + @Test public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 05ca00c91..c34430481 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -71,6 +71,19 @@ public void ignoredPathParameter() throws IOException { "/{a}/{b}").build()); } + @Test + public void allUndocumentedPathParametersCanBeIgnored() throws IOException { + this.snippet.expectPathParameters("ignore-all-undocumented").withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("b", + "two")); + new PathParametersSnippet( + Arrays.asList(parameterWithName("b").description("two")), + true).document(operationBuilder("ignore-all-undocumented") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") + .build()); + } + @Test public void missingOptionalPathParameter() throws IOException { this.snippet diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 16baa187c..a62c5de2c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -80,6 +80,17 @@ public void ignoredRequestParameter() throws IOException { .param("b", "bravo").build()); } + @Test + public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { + this.snippet.expectRequestParameters("ignore-all-undocumented").withContents( + tableWithHeader("Parameter", "Description").row("b", "two")); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("b").description("two")), true) + .document(operationBuilder("ignore-all-undocumented") + .request("http://localhost").param("a", "bravo") + .param("b", "bravo").build()); + } + @Test public void missingOptionalRequestParameter() throws IOException { this.snippet.expectRequestParameters("missing-optional-request-parameter") From a3e688352441172be325ee69b3466f0722ffdef0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 09:50:01 +0100 Subject: [PATCH 095/898] Apply Eclipse Mars (4.5.2) code formatting --- .../curl/CurlRequestSnippetTests.java | 19 ++++---- .../headers/RequestHeadersSnippetTests.java | 43 ++++++++--------- .../headers/ResponseHeadersSnippetTests.java | 29 ++++++----- .../http/HttpRequestSnippetTests.java | 19 ++++---- .../PrettyPrintingContentModifierTests.java | 2 +- .../payload/RequestFieldsSnippetTests.java | 43 +++++++++-------- .../payload/ResponseFieldsSnippetTests.java | 48 ++++++++++--------- .../request/PathParametersSnippetTests.java | 38 ++++++++------- .../RequestParametersSnippetTests.java | 21 ++++---- ...kMvcRestDocumentationIntegrationTests.java | 5 +- 10 files changed, 145 insertions(+), 122 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index 519644337..5e5b44c76 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -262,8 +262,8 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()) - .build()); + .part("metadata", "{\"description\": \"foo\"}".getBytes()) + .build()); } @Test @@ -279,9 +279,11 @@ public void multipartPostWithContentType() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png") + .build()); } @Test @@ -314,9 +316,10 @@ public void multipartPostWithParameters() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana") + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 279371693..5f493033c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -62,20 +62,18 @@ public void requestWithHeaders() throws IOException { .row("Accept", "two").row("Accept-Encoding", "three") .row("Accept-Language", "four").row("Cache-Control", "five") .row("Connection", "six")); - new RequestHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"), - headerWithName("Accept").description("two"), - headerWithName("Accept-Encoding").description("three"), - headerWithName("Accept-Language").description("four"), - headerWithName("Cache-Control").description("five"), - headerWithName("Connection").description("six"))) - .document(new OperationBuilder("request-with-headers", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .header("X-Test", "test") - .header("Accept", "*/*") - .header("Accept-Encoding", - "gzip, deflate") + new RequestHeadersSnippet(Arrays.asList( + headerWithName("X-Test").description("one"), + headerWithName("Accept").description("two"), + headerWithName("Accept-Encoding").description("three"), + headerWithName("Accept-Language").description("four"), + headerWithName("Cache-Control").description("five"), + headerWithName("Connection").description("six"))) + .document(new OperationBuilder("request-with-headers", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate") .header("Accept-Language", "en-US,en;q=0.5") .header("Cache-Control", "max-age=0") .header("Connection", "keep-alive").build()); @@ -143,14 +141,15 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")) .willReturn(snippetResource("request-headers-with-extra-column")); - new RequestHeadersSnippet(Arrays.asList( - headerWithName("X-Test").description("one") - .attributes(key("foo").value("alpha")), - headerWithName("Accept-Encoding").description("two") - .attributes(key("foo").value("bravo")), - headerWithName("Accept").description("three") - .attributes(key("foo").value("charlie")))) - .document(new OperationBuilder( + new RequestHeadersSnippet( + Arrays.asList( + headerWithName("X-Test").description("one") + .attributes(key("foo").value( + "alpha")), + headerWithName("Accept-Encoding").description("two") + .attributes(key("foo").value("bravo")), + headerWithName("Accept").description("three").attributes(key( + "foo").value("charlie")))).document(new OperationBuilder( "request-headers-with-custom-attributes", this.snippet.getOutputDirectory()) .attribute(TemplateEngine.class.getName(), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 8f5dd609f..41fe5fcd2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -72,9 +72,9 @@ public void responseWithHeaders() throws IOException { .header("X-Test", "test") .header("Content-Type", "application/json") - .header("Etag", "lskjadldj3ii32l2ij23") - .header("Cache-Control", "max-age=0") - .header("Vary", "User-Agent").build()); + .header("Etag", "lskjadldj3ii32l2ij23") + .header("Cache-Control", "max-age=0") + .header("Vary", "User-Agent").build()); } @Test @@ -107,15 +107,20 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("charlie")))) .document(new OperationBuilder( "response-headers-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .response().header("X-Test", "test") - .header("Content-Type", - "application/json") - .header("Etag", "lskjadldj3ii32l2ij23") - .build()); + this.snippet + .getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .response() + .header("X-Test", "test") + .header("Content-Type", + "application/json") + .header("Etag", + "lskjadldj3ii32l2ij23") + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 5175edd8c..ed0302323 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -92,8 +92,8 @@ public void postRequestWithContent() throws IOException { String content = "Hello, world"; this.snippet.expectHttpRequest("post-request-with-content") .withContents(httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet() .document(new OperationBuilder("post-request-with-content", @@ -154,8 +154,8 @@ public void putRequestWithContent() throws IOException { String content = "Hello, world"; this.snippet.expectHttpRequest("put-request-with-content") .withContents(httpRequest(RequestMethod.PUT, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(new OperationBuilder("put-request-with-content", this.snippet.getOutputDirectory()).request("http://localhost/foo") @@ -223,8 +223,8 @@ public void multipartPostWithParameters() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a", "apple", "avocado").param("b", "banana") - .part("image", "<< data >>".getBytes()).build()); + .param("a", "apple", "avocado").param("b", "banana") + .part("image", "<< data >>".getBytes()).build()); } @Test @@ -269,9 +269,10 @@ public void multipartPostWithContentType() throws IOException { .request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .build()); + .part("image", "<< data >>".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, + MediaType.IMAGE_PNG_VALUE) + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java index 84accb52a..319c37fbe 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/PrettyPrintingContentModifierTests.java @@ -56,7 +56,7 @@ public void prettyPrintXml() throws Exception { equalTo(String .format("%n" + "%n %n%n") - .getBytes())); + .getBytes())); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 40097f3df..52326fdb0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -61,8 +61,8 @@ public class RequestFieldsSnippetTests { 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")); + .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"), @@ -132,11 +132,13 @@ public void missingRequestField() throws IOException { @Test public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .optional())).document(new OperationBuilder( - "missing-optional-request-field-with-no-type", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{ }").build()); + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a.b").description("one").optional())) + .document(new OperationBuilder( + "missing-optional-request-field-with-no-type", + this.snippet.getOutputDirectory()) + .request("http://localhost").content("{ }") + .build()); } @Test @@ -175,14 +177,17 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("charlie")))) .document(new OperationBuilder( "request-fields-with-custom-descriptor-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .content( - "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); + this.snippet + .getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test @@ -228,8 +233,8 @@ public void requestFieldsWithListDescription() throws IOException { public void xmlRequestFields() throws IOException { this.snippet.expectRequestFields("xml-request") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a/b", "b", "one").row("a/c", "c", "two") - .row("a", "a", "three")); + .row("a/b", "b", "one").row("a/c", "c", "two").row("a", "a", + "three")); new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a/b").description("one").type("b"), @@ -241,7 +246,7 @@ public void xmlRequestFields() throws IOException { .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -283,7 +288,7 @@ public void missingXmlRequestField() throws IOException { .content("") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index a33a4fec4..6c3eff3dd 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -94,7 +94,7 @@ public void arrayResponseWithFields() throws IOException { .response() .content( "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") - .build()); + .build()); } @Test @@ -141,14 +141,17 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("charlie")))) .document(new OperationBuilder( "response-fields-with-custom-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .response() - .content( - "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); + this.snippet + .getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .response() + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test @@ -172,8 +175,8 @@ public void responseFieldsWithCustomAttributes() throws IOException { public void xmlResponseFields() throws IOException { this.snippet.expectResponseFields("xml-response") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a/b", "b", "one").row("a/c", "c", "two") - .row("a", "a", "three")); + .row("a/b", "b", "one").row("a/c", "c", "two").row("a", "a", + "three")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a/b").description("one").type("b"), fieldWithPath("a/c").description("two").type("c"), @@ -183,7 +186,7 @@ public void xmlResponseFields() throws IOException { .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -199,7 +202,7 @@ public void undocumentedXmlResponseField() throws IOException { .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -215,7 +218,7 @@ public void xmlAttribute() throws IOException { .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -231,7 +234,7 @@ public void missingXmlAttribute() throws IOException { .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -263,7 +266,7 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { .content("bar") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -273,15 +276,14 @@ public void documentedXmlAttributesAreRemoved() throws IOException { String.format("The following parts of the payload were not documented:" + "%nbar%n"))); new ResponseFieldsSnippet( - Arrays.asList( - fieldWithPath("a/@id").description("one") - .type("a"))).document(new OperationBuilder( - "documented-attribute-is-removed", + Arrays.asList(fieldWithPath("a/@id").description("one").type("a"))) + .document( + new OperationBuilder("documented-attribute-is-removed", this.snippet.getOutputDirectory()).response() .content("bar") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -308,7 +310,7 @@ public void missingXmlResponseField() throws IOException { .content("") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } @Test @@ -329,7 +331,7 @@ public void undocumentedXmlResponseFieldAndMissingXmlResponseField() .content("5") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) - .build()); + .build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 885ba390d..51b1aad96 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -79,7 +79,7 @@ public void missingPathParameter() throws IOException { .document(new OperationBuilder("missing-path-parameter", this.snippet.getOutputDirectory()).attribute( "org.springframework.restdocs.urlTemplate", "/") - .build()); + .build()); } @Test @@ -104,11 +104,13 @@ public void pathParameters() throws IOException { tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description") .row("a", "one").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))).document(new OperationBuilder( - "path-parameters", this.snippet.getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", - "/{a}/{b}") - .build()); + parameterWithName("b").description("two"))) + .document(new OperationBuilder( + "path-parameters", this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .build()); } @Test @@ -117,11 +119,14 @@ public void ignoredPathParameter() throws IOException { tableWithTitleAndHeader("/{a}/{b}", "Parameter", "Description").row("b", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), - parameterWithName("b").description("two"))).document(new OperationBuilder( - "ignored-path-parameter", this.snippet.getOutputDirectory()) - .attribute("org.springframework.restdocs.urlTemplate", - "/{a}/{b}") - .build()); + parameterWithName("b").description("two"))) + .document(new OperationBuilder( + "ignored-path-parameter", + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") + .build()); } @Test @@ -156,9 +161,10 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { parameterWithName("b").description("two").attributes( key("foo").value("bravo")))).document(new OperationBuilder( "path-parameters-with-custom-descriptor-attributes", - this.snippet.getOutputDirectory()).attribute( - "org.springframework.restdocs.urlTemplate", - "/{a}/{b}") + this.snippet.getOutputDirectory()) + .attribute( + "org.springframework.restdocs.urlTemplate", + "/{a}/{b}") .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .build()); @@ -176,8 +182,8 @@ public void pathParametersWithCustomAttributes() throws IOException { Arrays.asList( parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two") - .attributes(key("foo").value("bravo"))), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))).document( new OperationBuilder("path-parameters-with-custom-attributes", this.snippet.getOutputDirectory()) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index c4b05f355..97b4253bb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -151,13 +151,16 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException .attributes(key("foo").value("bravo")))) .document(new OperationBuilder( "request-parameters-with-custom-descriptor-attributes", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine( - resolver)) - .request("http://localhost") - .param("a", "alpha").param("b", "bravo") - .build()); + this.snippet + .getOutputDirectory()) + .attribute( + TemplateEngine.class + .getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .param("a", "alpha") + .param("b", "bravo").build()); } @Test @@ -172,8 +175,8 @@ public void requestParametersWithCustomAttributes() throws IOException { Arrays.asList( parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two") - .attributes(key("foo").value("bravo"))), + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo"))), attributes(key("title").value("The title"))).document( new OperationBuilder("request-parameters-with-custom-attributes", this.snippet.getOutputDirectory()) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index f37ce499a..86dd0f55c 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -217,9 +217,8 @@ public void responseFieldsSnippet() throws Exception { mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links", - responseFields(fieldWithPath("a") - .description("The description"), + .andDo(document("links", responseFields( + fieldWithPath("a").description("The description"), fieldWithPath("links").description("Links to other resources")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), From 96330b2e30ffe620b1a82ff734e1b797ccfa6cae Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 10:05:20 +0100 Subject: [PATCH 096/898] Improve testing of optional links and request and response fields --- .../hypermedia/LinksSnippetTests.java | 6 ++--- .../payload/RequestFieldsSnippetTests.java | 26 +++++++++++++++++++ .../payload/ResponseFieldsSnippetTests.java | 24 +++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 5f07e6ddf..fd0a4c5df 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -92,12 +92,12 @@ public void missingLink() throws IOException { } @Test - public void documentedOptionalLink() throws IOException { - this.snippet.expectLinks("documented-optional-link").withContents( + public void presentOptionalLink() throws IOException { + this.snippet.expectLinks("present-optional-link").withContents( tableWithHeader("Relation", "Description").row("foo", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(new OperationBuilder("documented-optional-link", + .document(new OperationBuilder("present-optional-link", this.snippet.getOutputDirectory()).build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 52326fdb0..216b5ab7b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -129,6 +129,32 @@ public void missingRequestField() throws IOException { .content("{}").build()); } + @Test + public void missingOptionalRequestField() throws IOException { + this.snippet.expectRequestFields("missing-optional-request-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", + "String", "one")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .type(JsonFieldType.STRING).optional())) + .document(new OperationBuilder("missing-optional-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost").content("{}") + .build()); + } + + @Test + public void presentOptionalRequestField() throws IOException { + this.snippet.expectRequestFields("present-optional-request-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", + "String", "one")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .type(JsonFieldType.STRING).optional())) + .document(new OperationBuilder("present-optional-request-field", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("{\"a\": { \"b\": \"bravo\"}}").build()); + } + @Test public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 6c3eff3dd..1356d7b3b 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -121,6 +121,30 @@ public void ignoredResponseField() throws IOException { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void missingOptionalResponseField() throws IOException { + this.snippet.expectResponseFields("missing-optional-response-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", + "String", "one")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .type(JsonFieldType.STRING).optional())) + .document(new OperationBuilder("missing-optional-response-field", + this.snippet.getOutputDirectory()).response() + .content("{}").build()); + } + + @Test + public void presentOptionalResponseField() throws IOException { + this.snippet.expectResponseFields("present-optional-response-field") + .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", + "String", "one")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") + .type(JsonFieldType.STRING).optional())) + .document(new OperationBuilder("present-optional-response-field", + this.snippet.getOutputDirectory()).response() + .content("{\"a\": { \"b\": \"bravo\"}}").build()); + } + @Test public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); From 5b32a3aa9805189190adebcf4e00dbb2a596bb9c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 10:34:02 +0100 Subject: [PATCH 097/898] Optional path and req param descriptors should count as documented Previously, optional path and request parameter descriptors were ignored when checking that all of the parameters that are present had been documented. This lead to a false negative if a request or path parameter was present and was documented with an optional descriptor. This commit uses all of the descriptors, not just those that are not optional, when checking for undocumented parameters. Closes gh-228 --- .../request/AbstractParametersSnippet.java | 2 +- .../request/PathParametersSnippetTests.java | 18 ++++++++++++++++++ .../request/RequestParametersSnippetTests.java | 11 +++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index 9d97737d5..f2d5bd99c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -124,7 +124,7 @@ private void verifyParameterDescriptors(Operation operation) { } else { undocumentedParameters = new HashSet<>(actualParameters); - undocumentedParameters.removeAll(expectedParameters); + undocumentedParameters.removeAll(this.descriptorsByName.keySet()); } Set missingParameters = new HashSet<>(expectedParameters); missingParameters.removeAll(actualParameters); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index eac8c9d14..054ed7bcf 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -99,6 +99,24 @@ public void missingOptionalPathParameter() throws IOException { "/{a}").build()); } + @Test + public void presentOptionalPathParameter() throws IOException { + this.snippet + .expectPathParameters( + "present-optional-path-parameter") + .withContents(tableWithTitleAndHeader( + this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" + : "`/{a}`", + "Parameter", "Description").row("a", "one")); + new PathParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").optional())) + .document(operationBuilder("present-optional-path-parameter") + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}") + .build()); + } + @Test public void pathParametersWithQueryString() throws IOException { this.snippet.expectPathParameters("path-parameters-with-query-string") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 380f1cc32..2bd69444d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -104,6 +104,17 @@ public void missingOptionalRequestParameter() throws IOException { .build()); } + @Test + public void presentOptionalRequestParameter() throws IOException { + this.snippet.expectRequestParameters("present-optional-request-parameter") + .withContents( + tableWithHeader("Parameter", "Description").row("a", "one")); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").optional())) + .document(operationBuilder("present-optional-request-parameter") + .request("http://localhost").param("a", "one").build()); + } + @Test public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); From 1c95232e6710e2a9a6fb81803b3809bcf55754f4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 09:48:11 +0100 Subject: [PATCH 098/898] Include optional attribute in model for path and req param descriptors --- .../request/AbstractParametersSnippet.java | 1 + .../RequestParametersSnippetTests.java | 19 +++++++++++++++++++ ...st-parameters-with-optional-column.snippet | 10 ++++++++++ ...st-parameters-with-optional-column.snippet | 5 +++++ 4 files changed, 35 insertions(+) create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java index f2d5bd99c..31d2913e3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/AbstractParametersSnippet.java @@ -189,6 +189,7 @@ protected Map createModelForDescriptor( Map model = new HashMap<>(); model.put("name", descriptor.getName()); model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); model.putAll(descriptor.getAttributes()); return model; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 2bd69444d..4c4c9a82c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -163,6 +163,25 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException .build()); } + @Test + public void requestParametersWithOptionalColumn() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-parameters")) + .willReturn(snippetResource("request-parameters-with-optional-column")); + this.snippet.expectRequestParameters("request-parameters-with-optional-column") + .withContents(tableWithHeader("Parameter", "Optional", "Description") + .row("a", "true", "one").row("b", "false", "two")); + + new RequestParametersSnippet(Arrays.asList( + parameterWithName("a").description("one").optional(), + parameterWithName("b").description("two"))).document( + operationBuilder("request-parameters-with-optional-column") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").param("a", "alpha") + .param("b", "bravo").build()); + } + @Test public void additionalDescriptors() throws IOException { this.snippet.expectRequestParameters("additional-descriptors") diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet new file mode 100644 index 000000000..70847ba1d --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parameters-with-optional-column.snippet @@ -0,0 +1,10 @@ +|=== +|Parameter|Optional|Description + +{{#parameters}} +|{{name}} +|{{optional}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet new file mode 100644 index 000000000..2f9cd0774 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parameters-with-optional-column.snippet @@ -0,0 +1,5 @@ +Parameter | Optional | Description +--------- | -------- | ----------- +{{#parameters}} +{{name}} | {{optional}} | {{description}} +{{/parameters}} \ No newline at end of file From 9e84922e096448d6606a9c78d72165a8df954ee3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 11:13:27 +0100 Subject: [PATCH 099/898] Upgrade samples to Spring Boot 1.3.3.RELEASE Closes gh-222 --- .../build/SampleBuildConfigurer.groovy | 4 ++-- samples/rest-assured/build.gradle | 7 ++++--- samples/rest-notes-slate/build.gradle | 7 ++++--- .../com/example/notes/RestNotesSlate.java | 8 ++------ samples/rest-notes-spring-data-rest/pom.xml | 3 +-- .../src/main/asciidoc/api-guide.adoc | 9 +++++++++ .../notes/RestNotesSpringDataRest.java | 10 +++------- .../com/example/notes/ApiDocumentation.java | 19 +++++++++++++++---- .../rest-notes-spring-hateoas/build.gradle | 7 ++++--- .../example/notes/RestNotesSpringHateoas.java | 10 +++------- samples/testng/build.gradle | 6 +++--- 11 files changed, 50 insertions(+), 40 deletions(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 7ce726de8..1be930cf8 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -59,8 +59,8 @@ public class SampleBuildConfigurer { } sampleBuild.doFirst { replaceVersion(new File(this.workingDir, 'build.gradle'), - "springRestdocsVersion = '.*'", - "springRestdocsVersion = '${project.version}'") + "ext\\['spring-restdocs.version'\\] = '.*'", + "ext['spring-restdocs.version'] = '${project.version}'") } } else if (new File(sampleDir, 'pom.xml').isFile()) { diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 9ad942399..f4c79f9b2 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' } } @@ -28,13 +28,14 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } +ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' + dependencies { compile 'org.springframework.boot:spring-boot-starter-web' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile "org.springframework.restdocs:spring-restdocs-restassured:$springRestdocsVersion" + testCompile "org.springframework.restdocs:spring-restdocs-restassured:${project.ext['spring-restdocs.version']}" } test { diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 79788c3d6..d30ca93ee 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' } } @@ -24,9 +24,10 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } +ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' + dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' compile 'org.springframework.boot:spring-boot-starter-data-rest' @@ -36,7 +37,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' } test { diff --git a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java index 32c5ce243..380bebf98 100644 --- a/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java +++ b/samples/rest-notes-slate/src/main/java/com/example/notes/RestNotesSlate.java @@ -17,13 +17,9 @@ package com.example.notes; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.boot.autoconfigure.SpringBootApplication; -@EnableAutoConfiguration -@ComponentScan -@EnableJpaRepositories +@SpringBootApplication public class RestNotesSlate { public static void main(String[] args) { diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index a874c40c1..5e1735ed4 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.2.7.RELEASE + 1.3.3.RELEASE @@ -50,7 +50,6 @@ org.springframework.restdocs spring-restdocs-mockmvc - ${spring-restdocs.version} test diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc index 50498df41..4f7697d70 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.adoc @@ -138,6 +138,10 @@ include::{snippets}/notes-list-example/curl-request.adoc[] include::{snippets}/notes-list-example/http-response.adoc[] +[[resources-notes-list-links]] +==== Links + +include::{snippets}/notes-list-example/links.adoc[] [[resources-notes-create]] @@ -183,6 +187,11 @@ include::{snippets}/tags-list-example/curl-request.adoc[] include::{snippets}/tags-list-example/http-response.adoc[] +[[resources-tags-list-links]] +==== Links + +include::{snippets}/tags-list-example/links.adoc[] + [[resources-tags-create]] diff --git a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java index 74b98dd0c..9df142549 100644 --- a/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java +++ b/samples/rest-notes-spring-data-rest/src/main/java/com/example/notes/RestNotesSpringDataRest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -17,13 +17,9 @@ package com.example.notes; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.boot.autoconfigure.SpringBootApplication; -@EnableAutoConfiguration -@ComponentScan -@EnableJpaRepositories +@SpringBootApplication public class RestNotesSpringDataRest { public static void main(String[] args) { diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index cbcaeb3fc..2db8fcb40 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -133,8 +133,12 @@ public void notesListExample() throws Exception { this.mockMvc.perform(get("/notes")) .andExpect(status().isOk()) .andDo(document("notes-list-example", + links( + linkWithRel("self").description("Canonical link for this resource"), + linkWithRel("profile").description("The ALPS profile for this resource")), responseFields( - fieldWithPath("_embedded.notes").description("An array of <>")))); + fieldWithPath("_embedded.notes").description("An array of <>"), + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -195,9 +199,11 @@ public void noteGetExample() throws Exception { .andExpect(jsonPath("body", is(note.get("body")))) .andExpect(jsonPath("_links.self.href", is(noteLocation))) .andExpect(jsonPath("_links.tags", is(notNullValue()))) + .andDo(print()) .andDo(document("note-get-example", links( - linkWithRel("self").description("This <>"), + linkWithRel("self").description("Canonical link for this <>"), + linkWithRel("note").description("This <>"), linkWithRel("tags").description("This note's tags")), responseFields( fieldWithPath("title").description("The title of the note"), @@ -217,8 +223,12 @@ public void tagsListExample() throws Exception { this.mockMvc.perform(get("/tags")) .andExpect(status().isOk()) .andDo(document("tags-list-example", + links( + linkWithRel("self").description("Canonical link for this resource"), + linkWithRel("profile").description("The ALPS profile for this resource")), responseFields( - fieldWithPath("_embedded.tags").description("An array of <>")))); + fieldWithPath("_embedded.tags").description("An array of <>"), + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -295,7 +305,8 @@ public void tagGetExample() throws Exception { .andExpect(jsonPath("name", is(tag.get("name")))) .andDo(document("tag-get-example", links( - linkWithRel("self").description("This <>"), + linkWithRel("self").description("Canonical link for this <>"), + linkWithRel("tag").description("This <>"), linkWithRel("notes").description("The <> that have this tag")), responseFields( fieldWithPath("name").description("The name of the tag"), diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 555d25ec3..7941829f9 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' } } @@ -28,9 +28,10 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } +ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' + dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' compile 'org.springframework.boot:spring-boot-starter-hateoas' @@ -40,7 +41,7 @@ dependencies { testCompile 'com.jayway.jsonpath:json-path' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' } test { diff --git a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java index c51ecee0d..9ad461587 100644 --- a/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java +++ b/samples/rest-notes-spring-hateoas/src/main/java/com/example/notes/RestNotesSpringHateoas.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2016 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. @@ -17,13 +17,9 @@ package com.example.notes; import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.boot.autoconfigure.SpringBootApplication; -@EnableAutoConfiguration -@ComponentScan -@EnableJpaRepositories +@SpringBootApplication public class RestNotesSpringHateoas { public static void main(String[] args) { diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index a6eeb77af..7683e955d 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' } } @@ -28,8 +28,8 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') - springRestdocsVersion = '1.1.0.BUILD-SNAPSHOT' } +ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' @@ -37,7 +37,7 @@ dependencies { exclude group: 'junit', module: 'junit;' } testCompile 'org.testng:testng:6.9.10' - testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$springRestdocsVersion" + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' } test { From e37d0ff560eb409fbdbedfd82cdb7859043578ea Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 11:33:05 +0100 Subject: [PATCH 100/898] Use Maven Central rather than JCenter --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index b6e05c519..8de250007 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { repositories { - jcenter() + mavenCentral() maven { url 'https://repo.spring.io/plugins-release' } maven { url 'https://plugins.gradle.org/m2/' } } @@ -15,7 +15,7 @@ buildscript { allprojects { group = 'org.springframework.restdocs' repositories { - jcenter() + mavenCentral() } } From 99b84e40f4980e4f95696fadedc351bf58fb1329 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 12:29:17 +0100 Subject: [PATCH 101/898] Output stacktrace when Gradle samples fails to build --- .../restdocs/build/SampleBuildConfigurer.groovy | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 5ddd17ab1..2d56e5a26 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -16,12 +16,14 @@ package org.springframework.restdocs.build +import org.gradle.StartParameter import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.tasks.Exec import org.gradle.api.tasks.Copy import org.gradle.api.tasks.GradleBuild +import org.gradle.logging.ShowStacktrace public class SampleBuildConfigurer { @@ -85,9 +87,12 @@ public class SampleBuildConfigurer { Task gradleBuild = project.tasks.create("${name}Gradle", GradleBuild) gradleBuild.description = "Builds the ${name} sample with Gradle" gradleBuild.group = "Build" - gradleBuild.dir = this.workingDir - gradleBuild.tasks = ['clean', 'build'] gradleBuild.dependsOn dependencies + StartParameter startParameter = new StartParameter() + startParameter.showStacktrace = ShowStacktrace.ALWAYS + startParameter.taskNames = ['clean', 'build'] + startParameter.currentDir = new File(this.workingDir) + gradleBuild.startParameter = startParameter gradleBuild.doFirst { replaceVersion(new File(this.workingDir, 'build.gradle'), From 6a5fd53910b3a2341d62ec657e2f0a9f1491ac56 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 12:47:22 +0100 Subject: [PATCH 102/898] Update Asciidoctor dependencies --- docs/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/build.gradle b/docs/build.gradle index 5241018c0..681796107 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.asciidoctor.convert' version '1.5.2' + id 'org.asciidoctor.convert' version '1.5.3' } dependencies { diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 6e057717d..95427ccbb 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -73,7 +73,7 @@ org.asciidoctor asciidoctor-maven-plugin - 1.5.2.1 + 1.5.3 generate-docs From 40a12127d7cf34e3d4f338b5549304183f7b897d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 13:24:32 +0100 Subject: [PATCH 103/898] Alternative approach to getting a stacktrace for a sample build failure --- .travis.yml | 2 +- .../restdocs/build/SampleBuildConfigurer.groovy | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index d84c55194..66f456e77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ language: java jdk: - oraclejdk7 script: - - "./gradlew build buildSamples" \ No newline at end of file + - "./gradlew build buildSamples --stacktrace" \ No newline at end of file diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 2d56e5a26..5ddd17ab1 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -16,14 +16,12 @@ package org.springframework.restdocs.build -import org.gradle.StartParameter import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.tasks.Exec import org.gradle.api.tasks.Copy import org.gradle.api.tasks.GradleBuild -import org.gradle.logging.ShowStacktrace public class SampleBuildConfigurer { @@ -87,12 +85,9 @@ public class SampleBuildConfigurer { Task gradleBuild = project.tasks.create("${name}Gradle", GradleBuild) gradleBuild.description = "Builds the ${name} sample with Gradle" gradleBuild.group = "Build" + gradleBuild.dir = this.workingDir + gradleBuild.tasks = ['clean', 'build'] gradleBuild.dependsOn dependencies - StartParameter startParameter = new StartParameter() - startParameter.showStacktrace = ShowStacktrace.ALWAYS - startParameter.taskNames = ['clean', 'build'] - startParameter.currentDir = new File(this.workingDir) - gradleBuild.startParameter = startParameter gradleBuild.doFirst { replaceVersion(new File(this.workingDir, 'build.gradle'), From 2417e9588d6f5222c990fbba39069f033be7e511 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 13:37:36 +0100 Subject: [PATCH 104/898] Disable Gradle daemon when building on Travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 66f456e77..69d0bdad0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,7 @@ sudo: false language: java jdk: - oraclejdk7 +env: + - GRADLE_OPTS=-Dorg.gradle.daemon=false script: - "./gradlew build buildSamples --stacktrace" \ No newline at end of file From 4d5f0df17e3a21b050367f02dcc13fe139181bda Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 13:49:08 +0100 Subject: [PATCH 105/898] Try to coax some useful diagnostics out of failing Travis build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 69d0bdad0..7736261c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,4 @@ jdk: env: - GRADLE_OPTS=-Dorg.gradle.daemon=false script: - - "./gradlew build buildSamples --stacktrace" \ No newline at end of file + - "./gradlew build buildSamples --stacktrace --debug" \ No newline at end of file From 4a67b80b15b5d80048b44d63d9708c885f6b7455 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 13:53:57 +0100 Subject: [PATCH 106/898] Try --info. Travis can't cope with volume produced by --debug --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7736261c1..0b31aca57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,4 @@ jdk: env: - GRADLE_OPTS=-Dorg.gradle.daemon=false script: - - "./gradlew build buildSamples --stacktrace --debug" \ No newline at end of file + - "./gradlew build buildSamples --stacktrace --info" \ No newline at end of file From bf9d977321f81f47ae99793ac1baebf1589306b5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:00:09 +0100 Subject: [PATCH 107/898] Try a different approach to switching off the Gradle daemon on Travis --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0b31aca57..8de78ff2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,5 @@ sudo: false language: java jdk: - oraclejdk7 -env: - - GRADLE_OPTS=-Dorg.gradle.daemon=false script: - - "./gradlew build buildSamples --stacktrace --info" \ No newline at end of file + - "./gradlew build buildSamples --stacktrace --info --no-daemon" \ No newline at end of file From a0236f67eb0a43a9902749d956c720e8ba52463a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:06:12 +0100 Subject: [PATCH 108/898] Build using Java 8 on Travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8de78ff2c..db7f567de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ sudo: false language: java jdk: - - oraclejdk7 + - oraclejdk8 script: - "./gradlew build buildSamples --stacktrace --info --no-daemon" \ No newline at end of file From f23d947cb520b81181dee79c75e055d7d5bf9dff Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:11:49 +0100 Subject: [PATCH 109/898] Use Spring Boot 1.3.3 to test REST Assured module --- spring-restdocs-restassured/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index 5f495f507..c9385ea82 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -10,7 +10,7 @@ dependencies { testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.2.5.RELEASE' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.3.RELEASE' testCompile 'org.springframework:spring-test' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') testRuntime 'commons-logging:commons-logging:1.2' From 75ea2101872c51f459dd1fc47cf1b2ebf7bdad7c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:29:20 +0100 Subject: [PATCH 110/898] Build against Spring Framework 4.2.x --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 419d5c8de..e08c78a52 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ sonarqube { } ext { - springVersion = '4.1.8.RELEASE' + springVersion = '4.2.5.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", From 60a02a431fde88cd5d2d3174a2d54d95bfd520fc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:37:58 +0100 Subject: [PATCH 111/898] Improve documentation of project's requirements Closes gh-217 --- README.md | 2 +- docs/src/docs/asciidoc/getting-started.adoc | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aaa0c6dcc..a1488ceb0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ To learn more about Spring REST Docs, please consult the [reference documentatio ## Building from source -Spring REST Docs requires Java 7 or later and is built using [Gradle][10]: +You will need Java 7 or later to build Spring REST Docs. It is built using [Gradle][10]: ``` ./gradlew build diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 8c0dba9d1..eb9c1fe5f 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -39,7 +39,18 @@ If you want to jump straight in, a number of sample applications are available: |=== +[[getting-started-requirements]] +=== Requirements +Spring REST Docs has the following minimum requirements: + +- Java 7 +- Spring Framework 4.2 + +Additionally, the `spring-restdocs-restassured` module has the following minimum +requirements: + +- REST Assured 2.8 [[getting-started-build-configuration]] === Build configuration From d362f3f41f1307700e2d1c2f3710c9a2b02b767c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 5 May 2016 14:47:03 +0100 Subject: [PATCH 112/898] Document support for relaxed snippets Closes gh-223 --- .../docs/asciidoc/documenting-your-api.adoc | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 9242fcbc3..7e53f1d99 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -49,6 +49,11 @@ response and the link has not been marked as optional. If you do not want to document a link, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described above. +Links can also be documented in a relaxed mode where any undocumented links will not cause +a test failure. To do so, use the `relaxedLinks` method on +`org.springframework.restdocs.hypermedia.HypermediaDocumentation`. This can be useful when +documenting a particular scenario where you only want to focus on a subset of the links. + [[documenting-your-api-hypermedia-link-formats]] @@ -144,6 +149,12 @@ treated as having been documented. If you do not want to document a field, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described above. +Fields can also be documented in a relaxed mode where any undocumented fields will not +cause a test failure. To do so, use the `relaxedRequestFields` and `relaxedResponseFields` +methods on +`org.springframework.restdocs.payload.PayloadDocumentation`. This can be useful when +documenting a particular scenario where you only want to focus on a subset of the payload. + TIP: By default, Spring REST Docs will assume that the payload you are documenting is JSON. If you want to document an XML payload the content type of the request or response must be compatible with `application/xml`. @@ -454,6 +465,12 @@ If you do not want to document a request parameter, you can mark it as ignored. prevent it from appearing in the generated snippet while avoiding the failure described above. +Request parameters can also be documented in a relaxed mode where any undocumented +parameters will not cause a test failure. To do so, use the `relaxedRequestParameters` +method on `org.springframework.restdocs.request.RequestDocumentation`. This can be useful +when documenting a particular scenario where you only want to focus on a subset of the +request parameters. + [[documenting-your-api-path-parameters]] @@ -498,6 +515,12 @@ When documenting path parameters, the test will fail if an undocumented path par is used in the request. Similarly, the test will also fail if a documented path parameter is not found in the request and the path parameter has not been marked as optional. +Path parameters can also be documented in a relaxed mode where any undocumented +parameters will not cause a test failure. To do so, use the `relaxedPathParameters` +method on `org.springframework.restdocs.request.RequestDocumentation`. This can be useful +when documenting a particular scenario where you only want to focus on a subset of the +path parameters. + If you do not want to document a path parameter, you can mark it as ignored. This will prevent it from appearing in the generated snippet while avoiding the failure described above. From 63a6ad2c9166133e53384747f2581632ff172bf2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 11 May 2016 14:50:39 +0100 Subject: [PATCH 113/898] Improve readability by using monospaced font in various snippets This commit updates a number of the snippets to use a monospaced font for parts of their output. The changes are: - Links snippet: rel - Path parameters: name - Request fields: path and type - Request headers: name - Request parameters: name - Response fields: path and type - Response headers: name Closes gh-230 --- .../asciidoctor/default-links.snippet | 2 +- .../default-path-parameters.snippet | 2 +- .../default-request-fields.snippet | 4 +- .../default-request-headers.snippet | 2 +- .../default-request-parameters.snippet | 2 +- .../default-response-fields.snippet | 4 +- .../default-response-headers.snippet | 2 +- .../templates/markdown/default-links.snippet | 2 +- .../markdown/default-path-parameters.snippet | 2 +- .../markdown/default-request-fields.snippet | 2 +- .../markdown/default-request-headers.snippet | 2 +- .../default-request-parameters.snippet | 2 +- .../markdown/default-response-fields.snippet | 2 +- .../markdown/default-response-headers.snippet | 2 +- .../headers/RequestHeadersSnippetTests.java | 20 ++++--- .../headers/ResponseHeadersSnippetTests.java | 18 +++--- .../hypermedia/LinksSnippetTests.java | 20 +++---- .../payload/RequestFieldsSnippetTests.java | 37 ++++++------ .../payload/ResponseFieldsSnippetTests.java | 57 ++++++++++--------- .../request/PathParametersSnippetTests.java | 14 ++--- .../RequestParametersSnippetTests.java | 20 +++---- 21 files changed, 112 insertions(+), 106 deletions(-) diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet index b8b452903..132643d8e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet @@ -2,7 +2,7 @@ |Relation|Description {{#links}} -|{{rel}} +|`{{rel}}` |{{description}} {{/links}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet index 66db38621..a6cc9f8d4 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet @@ -3,7 +3,7 @@ |Parameter|Description {{#parameters}} -|{{name}} +|`{{name}}` |{{description}} {{/parameters}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet index 497f5a03a..41e763e74 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet @@ -2,8 +2,8 @@ |Path|Type|Description {{#fields}} -|{{path}} -|{{type}} +|`{{path}}` +|`{{type}}` |{{description}} {{/fields}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet index 5f14875dc..f3d660355 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet @@ -2,7 +2,7 @@ |Name|Description {{#headers}} -|{{name}} +|`{{name}}` |{{description}} {{/headers}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet index 067e1fb83..f338b345f 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet @@ -2,7 +2,7 @@ |Parameter|Description {{#parameters}} -|{{name}} +|`{{name}}` |{{description}} {{/parameters}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet index 497f5a03a..41e763e74 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet @@ -2,8 +2,8 @@ |Path|Type|Description {{#fields}} -|{{path}} -|{{type}} +|`{{path}}` +|`{{type}}` |{{description}} {{/fields}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet index 5f14875dc..f3d660355 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet @@ -2,7 +2,7 @@ |Name|Description {{#headers}} -|{{name}} +|`{{name}}` |{{description}} {{/headers}} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet index 116d8eabe..8c690d0f1 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-links.snippet @@ -1,5 +1,5 @@ Relation | Description -------- | ----------- {{#links}} -{{rel}} | {{description}} +`{{rel}}` | {{description}} {{/links}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet index f1811879f..76dfc572e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-path-parameters.snippet @@ -3,5 +3,5 @@ Parameter | Description --------- | ----------- {{#parameters}} -{{name}} | {{description}} +`{{name}}` | {{description}} {{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet index e8d2957f3..27a4e4379 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-fields.snippet @@ -1,5 +1,5 @@ Path | Type | Description ---- | ---- | ----------- {{#fields}} -{{path}} | {{type}} | {{description}} +`{{path}}` | `{{type}}` | {{description}} {{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet index f8c5ed27e..7bbe694df 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-headers.snippet @@ -1,5 +1,5 @@ Name | Description ---- | ----------- {{#headers}} -{{name}} | {{description}} +`{{name}}` | {{description}} {{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet index 09a8a1992..681daaa8e 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parameters.snippet @@ -1,5 +1,5 @@ Parameter | Description --------- | ----------- {{#parameters}} -{{name}} | {{description}} +`{{name}}` | {{description}} {{/parameters}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet index e8d2957f3..27a4e4379 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-fields.snippet @@ -1,5 +1,5 @@ Path | Type | Description ---- | ---- | ----------- {{#fields}} -{{path}} | {{type}} | {{description}} +`{{path}}` | `{{type}}` | {{description}} {{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet index f8c5ed27e..7bbe694df 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-headers.snippet @@ -1,5 +1,5 @@ Name | Description ---- | ----------- {{#headers}} -{{name}} | {{description}} +`{{name}}` | {{description}} {{/headers}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 2f97aee88..7a0c9edd0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -50,10 +50,11 @@ public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestWithHeaders() throws IOException { this.snippet.expectRequestHeaders("request-with-headers") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") - .row("Accept", "two").row("Accept-Encoding", "three") - .row("Accept-Language", "four").row("Cache-Control", "five") - .row("Connection", "six")); + .withContents(tableWithHeader("Name", "Description") + .row("`X-Test`", "one").row("`Accept`", "two") + .row("`Accept-Encoding`", "three") + .row("`Accept-Language`", "four").row("`Cache-Control`", "five") + .row("`Connection`", "six")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), @@ -74,7 +75,7 @@ public void requestWithHeaders() throws IOException { public void caseInsensitiveRequestHeaders() throws IOException { this.snippet.expectRequestHeaders("case-insensitive-request-headers") .withContents( - tableWithHeader("Name", "Description").row("X-Test", "one")); + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) .document(operationBuilder("case-insensitive-request-headers") @@ -143,10 +144,11 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { this.snippet.expectRequestHeaders("additional-descriptors") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") - .row("Accept", "two").row("Accept-Encoding", "three") - .row("Accept-Language", "four").row("Cache-Control", "five") - .row("Connection", "six")); + .withContents(tableWithHeader("Name", "Description") + .row("`X-Test`", "one").row("`Accept`", "two") + .row("`Accept-Encoding`", "three") + .row("`Accept-Language`", "four").row("`Cache-Control`", "five") + .row("`Connection`", "six")); HeaderDocumentation .requestHeaders(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 463f631c4..d7bf553ac 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -49,10 +49,10 @@ public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders("response-headers") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") - .row("Content-Type", "two").row("Etag", "three") - .row("Cache-Control", "five").row("Vary", "six")); + this.snippet.expectResponseHeaders("response-headers").withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one") + .row("`Content-Type`", "two").row("`Etag`", "three") + .row("`Cache-Control`", "five").row("`Vary`", "six")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Content-Type").description("two"), @@ -73,7 +73,7 @@ public void responseWithHeaders() throws IOException { public void caseInsensitiveResponseHeaders() throws IOException { this.snippet.expectResponseHeaders("case-insensitive-response-headers") .withContents( - tableWithHeader("Name", "Description").row("X-Test", "one")); + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) .document(operationBuilder("case-insensitive-response-headers") @@ -136,10 +136,10 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseHeaders("additional-descriptors") - .withContents(tableWithHeader("Name", "Description").row("X-Test", "one") - .row("Content-Type", "two").row("Etag", "three") - .row("Cache-Control", "five").row("Vary", "six")); + this.snippet.expectResponseHeaders("additional-descriptors").withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one") + .row("`Content-Type`", "two").row("`Etag`", "three") + .row("`Cache-Control`", "five").row("`Vary`", "six")); HeaderDocumentation .responseHeaders(headerWithName("X-Test").description("one"), headerWithName("Content-Type").description("two"), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index cf128d212..46f73b303 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -47,7 +47,7 @@ public LinksSnippetTests(String name, TemplateFormat templateFormat) { @Test public void ignoredLink() throws IOException { this.snippet.expectLinks("ignored-link").withContents( - tableWithHeader("Relation", "Description").row("b", "Link b")); + tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), @@ -59,7 +59,7 @@ public void ignoredLink() throws IOException { @Test public void allUndocumentedLinksCanBeIgnored() throws IOException { this.snippet.expectLinks("ignore-all-undocumented").withContents( - tableWithHeader("Relation", "Description").row("b", "Link b")); + tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), @@ -70,7 +70,7 @@ public void allUndocumentedLinksCanBeIgnored() throws IOException { @Test public void presentOptionalLink() throws IOException { this.snippet.expectLinks("present-optional-link").withContents( - tableWithHeader("Relation", "Description").row("foo", "bar")); + tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) .document(operationBuilder("present-optional-link").build()); @@ -79,7 +79,7 @@ public void presentOptionalLink() throws IOException { @Test public void missingOptionalLink() throws IOException { this.snippet.expectLinks("missing-optional-link").withContents( - tableWithHeader("Relation", "Description").row("foo", "bar")); + tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) .document(operationBuilder("missing-optional-link").build()); @@ -88,8 +88,8 @@ public void missingOptionalLink() throws IOException { @Test public void documentedLinks() throws IOException { this.snippet.expectLinks("documented-links") - .withContents(tableWithHeader("Relation", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") + .row("`b`", "two")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), @@ -101,8 +101,8 @@ public void documentedLinks() throws IOException { @Test public void linkDescriptionFromTitleInPayload() throws IOException { this.snippet.expectLinks("link-description-from-title-in-payload") - .withContents(tableWithHeader("Relation", "Description").row("a", "one") - .row("b", "Link b")); + .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") + .row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), new Link("b", "bravo", "Link b")), @@ -162,8 +162,8 @@ public void linksWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { this.snippet.expectLinks("additional-descriptors") - .withContents(tableWithHeader("Relation", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") + .row("`b`", "two")); HypermediaDocumentation .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index b7e0c872d..eac0b8438 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -51,8 +51,8 @@ public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { 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")); + .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"), @@ -67,8 +67,9 @@ public void mapRequestWithFields() throws IOException { 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")); + .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"), @@ -83,8 +84,8 @@ public void arrayRequestWithFields() throws IOException { @Test public void ignoredRequestField() throws IOException { this.snippet.expectRequestFields("ignored-request-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("b", - "Number", "Field b")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) @@ -96,8 +97,8 @@ public void ignoredRequestField() throws IOException { @Test public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { this.snippet.expectRequestFields("ignore-all-undocumented") - .withContents(tableWithHeader("Path", "Type", "Description").row("b", - "Number", "Field b")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true).document( operationBuilder("ignore-all-undocumented") @@ -108,8 +109,8 @@ public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { @Test public void missingOptionalRequestField() throws IOException { this.snippet.expectRequestFields("missing-optional-request-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", + "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) .document(operationBuilder("missing-optional-request-field") @@ -119,8 +120,8 @@ public void missingOptionalRequestField() throws IOException { @Test public void presentOptionalRequestField() throws IOException { this.snippet.expectRequestFields("present-optional-request-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", + "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) .document(operationBuilder("present-optional-request-field") @@ -179,8 +180,8 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { public void xmlRequestFields() throws IOException { this.snippet.expectRequestFields("xml-request") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a/b", "b", "one").row("a/c", "c", "two").row("a", "a", - "three")); + .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", + "`a`", "three")); new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a/b").description("one").type("b"), @@ -199,8 +200,8 @@ public void xmlRequestFields() throws IOException { public void additionalDescriptors() throws IOException { this.snippet.expectRequestFields("additional-descriptors") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a.b", "Number", "one").row("a.c", "String", "two").row("a", - "Object", "three")); + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); PayloadDocumentation .requestFields(fieldWithPath("a.b").description("one"), @@ -215,8 +216,8 @@ public void additionalDescriptors() throws IOException { public void prefixedAdditionalDescriptors() throws IOException { this.snippet.expectRequestFields("prefixed-additional-descriptors") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a", "Object", "one").row("a.b", "Number", "two").row("a.c", - "String", "three")); + .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index fb97e3511..574899a09 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -51,10 +51,11 @@ public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { 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")); + .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")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), fieldWithPath("assets").description("three"), @@ -72,8 +73,9 @@ public void mapResponseWithFields() throws IOException { 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")); + .row("`[]a.b`", "`Number`", "one") + .row("`[]a.c`", "`String`", "two") + .row("`[]a`", "`Object`", "three")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a").description("three"))).document( @@ -86,8 +88,8 @@ public void arrayResponseWithFields() throws IOException { @Test public void arrayResponse() throws IOException { this.snippet.expectResponseFields("array-response") - .withContents(tableWithHeader("Path", "Type", "Description").row("[]", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`[]`", + "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) .document(operationBuilder("array-response").response() .content("[\"a\", \"b\", \"c\"]").build()); @@ -96,8 +98,8 @@ public void arrayResponse() throws IOException { @Test public void ignoredResponseField() throws IOException { this.snippet.expectResponseFields("ignored-response-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("b", - "Number", "Field b")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) @@ -108,8 +110,8 @@ public void ignoredResponseField() throws IOException { @Test public void allUndocumentedFieldsCanBeIgnored() throws IOException { this.snippet.expectResponseFields("ignore-all-undocumented") - .withContents(tableWithHeader("Path", "Type", "Description").row("b", - "Number", "Field b")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("b").description("Field b")), true) @@ -136,8 +138,8 @@ public void responseFieldsWithCustomAttributes() throws IOException { @Test public void missingOptionalResponseField() throws IOException { this.snippet.expectResponseFields("missing-optional-response-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", + "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) .document(operationBuilder("missing-optional-response-field") @@ -147,8 +149,8 @@ public void missingOptionalResponseField() throws IOException { @Test public void presentOptionalResponseField() throws IOException { this.snippet.expectResponseFields("present-optional-response-field") - .withContents(tableWithHeader("Path", "Type", "Description").row("a.b", - "String", "one")); + .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", + "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())).document( operationBuilder("present-optional-response-field").response() @@ -186,8 +188,8 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { public void xmlResponseFields() throws IOException { this.snippet.expectResponseFields("xml-response") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a/b", "b", "one").row("a/c", "c", "two").row("a", "a", - "three")); + .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", + "`a`", "three")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a/b").description("one").type("b"), fieldWithPath("a/c").description("two").type("c"), @@ -204,7 +206,7 @@ public void xmlResponseFields() throws IOException { public void xmlAttribute() throws IOException { this.snippet.expectResponseFields("xml-attribute") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a", "b", "one").row("a/@id", "c", "two")); + .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c"))) @@ -220,7 +222,7 @@ public void xmlAttribute() throws IOException { public void missingOptionalXmlAttribute() throws IOException { this.snippet.expectResponseFields("missing-optional-xml-attribute") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a", "b", "one").row("a/@id", "c", "two")); + .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c").optional())) @@ -235,7 +237,7 @@ public void missingOptionalXmlAttribute() throws IOException { @Test public void undocumentedAttributeDoesNotCauseFailure() throws IOException { this.snippet.expectResponseFields("undocumented-attribute").withContents( - tableWithHeader("Path", "Type", "Description").row("a", "a", "one")); + tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("a"))) .document(operationBuilder("undocumented-attribute").response() @@ -249,10 +251,11 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { public void additionalDescriptors() throws IOException { this.snippet.expectResponseFields("additional-descriptors") .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")); + .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")); PayloadDocumentation .responseFields(fieldWithPath("id").description("one"), fieldWithPath("date").description("two"), @@ -270,8 +273,8 @@ public void additionalDescriptors() throws IOException { public void prefixedAdditionalDescriptors() throws IOException { this.snippet.expectResponseFields("prefixed-additional-descriptors") .withContents(tableWithHeader("Path", "Type", "Description") - .row("a", "Object", "one").row("a.b", "Number", "two").row("a.c", - "String", "three")); + .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); PayloadDocumentation.responseFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index 054ed7bcf..c5f700654 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -51,7 +51,7 @@ public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { public void pathParameters() throws IOException { this.snippet.expectPathParameters("path-parameters").withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("a", "one").row("b", "two")); + .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) .document(operationBuilder("path-parameters").attribute( @@ -62,7 +62,7 @@ public void pathParameters() throws IOException { @Test public void ignoredPathParameter() throws IOException { this.snippet.expectPathParameters("ignored-path-parameter").withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("b", + tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) @@ -74,7 +74,7 @@ public void ignoredPathParameter() throws IOException { @Test public void allUndocumentedPathParametersCanBeIgnored() throws IOException { this.snippet.expectPathParameters("ignore-all-undocumented").withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("b", + tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) @@ -91,7 +91,7 @@ public void missingOptionalPathParameter() throws IOException { .withContents(tableWithTitleAndHeader( this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", - "Parameter", "Description").row("a", "one").row("b", "two")); + "Parameter", "Description").row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two").optional())).document( operationBuilder("missing-optional-path-parameter").attribute( @@ -107,7 +107,7 @@ public void presentOptionalPathParameter() throws IOException { .withContents(tableWithTitleAndHeader( this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", - "Parameter", "Description").row("a", "one")); + "Parameter", "Description").row("`a`", "one")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) .document(operationBuilder("present-optional-path-parameter") @@ -122,7 +122,7 @@ public void pathParametersWithQueryString() throws IOException { this.snippet.expectPathParameters("path-parameters-with-query-string") .withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("a", "one").row("b", "two")); + .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))).document( operationBuilder("path-parameters-with-query-string").attribute( @@ -183,7 +183,7 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { public void additionalDescriptors() throws IOException { this.snippet.expectPathParameters("additional-descriptors").withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("a", "one").row("b", "two")); + .row("`a`", "one").row("`b`", "two")); RequestDocumentation.pathParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) .document(operationBuilder("additional-descriptors") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 4c4c9a82c..94135a3bf 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -48,8 +48,8 @@ public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) @Test public void requestParameters() throws IOException { this.snippet.expectRequestParameters("request-parameters") - .withContents(tableWithHeader("Parameter", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) @@ -62,7 +62,7 @@ public void requestParameters() throws IOException { public void requestParameterWithNoValue() throws IOException { this.snippet.expectRequestParameters("request-parameter-with-no-value") .withContents( - tableWithHeader("Parameter", "Description").row("a", "one")); + tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) .document(operationBuilder("request-parameter-with-no-value") @@ -72,7 +72,7 @@ public void requestParameterWithNoValue() throws IOException { @Test public void ignoredRequestParameter() throws IOException { this.snippet.expectRequestParameters("ignored-request-parameter").withContents( - tableWithHeader("Parameter", "Description").row("b", "two")); + tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) .document(operationBuilder("ignored-request-parameter") @@ -83,7 +83,7 @@ public void ignoredRequestParameter() throws IOException { @Test public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { this.snippet.expectRequestParameters("ignore-all-undocumented").withContents( - tableWithHeader("Parameter", "Description").row("b", "two")); + tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) .document(operationBuilder("ignore-all-undocumented") @@ -94,8 +94,8 @@ public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { @Test public void missingOptionalRequestParameter() throws IOException { this.snippet.expectRequestParameters("missing-optional-request-parameter") - .withContents(tableWithHeader("Parameter", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional(), parameterWithName("b").description("two"))).document( @@ -108,7 +108,7 @@ public void missingOptionalRequestParameter() throws IOException { public void presentOptionalRequestParameter() throws IOException { this.snippet.expectRequestParameters("present-optional-request-parameter") .withContents( - tableWithHeader("Parameter", "Description").row("a", "one")); + tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) .document(operationBuilder("present-optional-request-parameter") @@ -185,8 +185,8 @@ public void requestParametersWithOptionalColumn() throws IOException { @Test public void additionalDescriptors() throws IOException { this.snippet.expectRequestParameters("additional-descriptors") - .withContents(tableWithHeader("Parameter", "Description").row("a", "one") - .row("b", "two")); + .withContents(tableWithHeader("Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); RequestDocumentation.requestParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) .document(operationBuilder("additional-descriptors") From fc3ae3b4a285e112e59b14423eb75f9744ac467b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 12 May 2016 15:21:16 +0100 Subject: [PATCH 114/898] Add support for documenting the parts of a multipart request Closes gh-161 --- .../docs/asciidoc/documenting-your-api.adoc | 49 +++++ .../com/example/mockmvc/RequestParts.java | 41 ++++ .../com/example/restassured/RequestParts.java | 42 ++++ .../request/RequestDocumentation.java | 90 ++++++++ .../request/RequestPartDescriptor.java | 73 +++++++ .../restdocs/request/RequestPartsSnippet.java | 206 ++++++++++++++++++ .../asciidoctor/default-request-parts.snippet | 9 + .../markdown/default-request-parts.snippet | 5 + .../RequestPartsSnippetFailureTests.java | 83 +++++++ .../request/RequestPartsSnippetTests.java | 183 ++++++++++++++++ .../restdocs/test/ExpectedSnippet.java | 5 + .../request-parts-with-extra-column.snippet | 10 + ...request-parts-with-optional-column.snippet | 10 + .../request-parts-with-title.snippet | 10 + .../request-parts-with-extra-column.snippet | 5 + ...request-parts-with-optional-column.snippet | 5 + .../markdown/request-parts-with-title.snippet | 6 + ...kMvcRestDocumentationIntegrationTests.java | 31 +++ .../restdocs/templates/request-parts.snippet | 9 + ...uredRestDocumentationIntegrationTests.java | 18 ++ 20 files changed, 890 insertions(+) create mode 100644 docs/src/test/java/com/example/mockmvc/RequestParts.java create mode 100644 docs/src/test/java/com/example/restassured/RequestParts.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parts.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-optional-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-title.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-extra-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-optional-column.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-title.snippet create mode 100644 spring-restdocs-mockmvc/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 7e53f1d99..d1a27442b 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -527,6 +527,55 @@ above. +[[documenting-your-api-request-parts]] +=== Request parts + +The parts of a multipart request can be documenting using `requestParts`. For example: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestParts.java[tags=request-parts] +---- +<1> Perform a `POST` request with a single part named `file`. +<2> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses + the static `requestParts` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<3> Document the part named `file`. Uses the static `partWithName` method on + `org.springframework.restdocs.request.RequestDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestParts.java[tags=request-parts] +---- +<1> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses + the static `requestParts` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<2> Document the part named `file`. Uses the static `partWithName` method on + `org.springframework.restdocs.request.RequestDocumentation`. +<3> Configure the request with the part named `file`. +<4> Perform the `POST` request to `/upload`. + +The result is a snippet named `request-parts.adoc` that contains a table describing the +request parts that are supported by the resource. + +When documenting request parts, the test will fail if an undocumented part is used in the +request. Similarly, the test will also fail if a documented part is not found in the +request and the part has not been marked as optional. + +Request parts can also be documented in a relaxed mode where any undocumented +parts will not cause a test failure. To do so, use the `relaxedRequestParts` method on +`org.springframework.restdocs.request.RequestDocumentation`. This can be useful +when documenting a particular scenario where you only want to focus on a subset of the +request parts. + +If you do not want to document a request part, you can mark it as ignored. This will +prevent it from appearing in the generated snippet while avoiding the failure described +above. + + + [[documenting-your-api-http-headers]] === HTTP headers diff --git a/docs/src/test/java/com/example/mockmvc/RequestParts.java b/docs/src/test/java/com/example/mockmvc/RequestParts.java new file mode 100644 index 000000000..90210d50f --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/RequestParts.java @@ -0,0 +1,41 @@ +/* + * Copyright 2014-2016 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 com.example.mockmvc; + +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class RequestParts { + + private MockMvc mockMvc; + + public void upload() throws Exception { + // tag::request-parts[] + this.mockMvc.perform(fileUpload("/upload").file("file", "example".getBytes())) // <1> + .andExpect(status().isOk()) + .andDo(document("upload", requestParts( // <2> + partWithName("file").description("The file to upload")) // <3> + )); + // end::request-parts[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestParts.java b/docs/src/test/java/com/example/restassured/RequestParts.java new file mode 100644 index 000000000..c6a3f8c1f --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestParts.java @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestParts { + + private RequestSpecification spec; + + public void upload() throws Exception { + // tag::request-parts[] + RestAssured.given(this.spec) + .filter(document("users", requestParts( // <1> + partWithName("file").description("The file to upload")))) // <2> + .multiPart("file", "example") // <3> + .when().post("/upload") // <4> + .then().statusCode(is(200)); + // end::request-parts[] + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 4c4aee73d..4fec764af 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -43,6 +43,17 @@ public static ParameterDescriptor parameterWithName(String name) { return new ParameterDescriptor(name); } + /** + * Creates a {@link RequestPartDescriptor} that describes a request part with the + * given {@code name}. + * + * @param name The name of the request part + * @return a {@link RequestPartDescriptor} ready for further configuration + */ + public static RequestPartDescriptor partWithName(String name) { + return new RequestPartDescriptor(name); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The parameters will be documented using the given @@ -207,4 +218,83 @@ public static RequestParametersSnippet relaxedRequestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The parts will be documented using the given {@code descriptors}. + *

          + * If a part is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part + * is documented, is not marked as optional, and is not present in the request, a + * failure will also occur. + *

          + * If you do not want to document a part, a part descriptor can be marked as + * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param descriptors The descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet requestParts(RequestPartDescriptor... descriptors) { + return new RequestPartsSnippet(Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The parameters will be documented using the given {@code descriptors}. + *

          + * If a part is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented parts will be ignored. + * + * @param descriptors The descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet relaxedRequestParts( + RequestPartDescriptor... descriptors) { + return new RequestPartsSnippet(Arrays.asList(descriptors), true); + } + + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The given {@code attributes} will be available during snippet rendering + * and the parts will be documented using the given {@code descriptors}. + *

          + * If a part is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part + * is documented, is not marked as optional, and is not present in the request, a + * failure will also occur. + *

          + * If you do not want to document a part, a part descriptor can be marked as + * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet requestParts(Map attributes, + RequestPartDescriptor... descriptors) { + return new RequestPartsSnippet(Arrays.asList(descriptors), attributes); + } + + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The given {@code attributes} will be available during snippet rendering + * and the parts will be documented using the given {@code descriptors}. + *

          + * If a part is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented parts will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestPartsSnippet relaxedRequestParts(Map attributes, + RequestPartDescriptor... descriptors) { + return new RequestPartsSnippet(Arrays.asList(descriptors), attributes, true); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java new file mode 100644 index 000000000..789649607 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2014-2016 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 org.springframework.restdocs.snippet.IgnorableDescriptor; + +/** + * A descriptor of a request part. + * + * @author Andy Wilkinson + * @see RequestDocumentation#partWithName + */ +public class RequestPartDescriptor extends IgnorableDescriptor { + + private final String name; + + private boolean optional; + + /** + * Creates a new {@code RequestPartDescriptor} describing the request part with the + * given {@code name}. + * + * @param name the name of the request part + */ + protected RequestPartDescriptor(String name) { + this.name = name; + } + + /** + * Marks the request part as optional. + * + * @return {@code this} + */ + public final RequestPartDescriptor optional() { + this.optional = true; + return this; + } + + /** + * Returns the name of the request part being described by this descriptor. + * + * @return the name of the parameter + */ + public final String getName() { + return this.name; + } + + /** + * Returns {@code true} if the described request part is optional, otherwise + * {@code false}. + * + * @return {@code true} if the described request part is optional, otherwise + * {@code false} + */ + public final boolean isOptional() { + return this.optional; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java new file mode 100644 index 000000000..1a5d30597 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java @@ -0,0 +1,206 @@ +/* + * Copyright 2014-2016 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.Assert; + +/** + * A {@link Snippet} that documents the request parts supported by a RESTful resource. + * + * @author Andy Wilkinson + * @see RequestDocumentation#requestParts(RequestPartDescriptor...) + * @see RequestDocumentation#requestParts(Map, RequestPartDescriptor...) + * @see RequestDocumentation#relaxedRequestParts(RequestPartDescriptor...) + * @see RequestDocumentation#relaxedRequestParts(Map, RequestPartDescriptor...) + */ +public class RequestPartsSnippet extends TemplatedSnippet { + + private final Map descriptorsByName = new LinkedHashMap<>(); + + private final boolean ignoreUndocumentedParts; + + /** + * Creates a new {@code RequestPartsSnippet} that will document the request's parts + * using the given {@code descriptors}. Undocumented parts will trigger a failure. + * + * @param descriptors the parameter descriptors + */ + protected RequestPartsSnippet(List descriptors) { + this(descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartsSnippet} that will document the request's parts + * using the given {@code descriptors}. If {@code ignoreUndocumentedParts} is + * {@code true}, undocumented parts will be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param ignoreUndocumentedParts whether undocumented parts should be ignored + */ + protected RequestPartsSnippet(List descriptors, + boolean ignoreUndocumentedParts) { + this(descriptors, null, ignoreUndocumentedParts); + } + + /** + * Creates a new {@code RequestPartsSnippet} that will document the request's parts + * using the given {@code descriptors}. The given {@code attributes} will be included + * in the model during template rendering. Undocumented parts will trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + */ + protected RequestPartsSnippet(List descriptors, + Map attributes) { + this(descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestPartsSnippet} that will document the request's parts + * using the given {@code descriptors}. The given {@code attributes} will be included + * in the model during template rendering. If {@code ignoreUndocumentedParts} is + * {@code true}, undocumented parts will be ignored and will not trigger a failure. + * + * @param descriptors the parameter descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedParts whether undocumented parts should be ignored + */ + protected RequestPartsSnippet(List descriptors, + Map attributes, boolean ignoreUndocumentedParts) { + super("request-parts", attributes); + for (RequestPartDescriptor descriptor : descriptors) { + Assert.notNull(descriptor.getName(), + "Request part descriptors must have a name"); + if (!descriptor.isIgnored()) { + Assert.notNull(descriptor.getDescription(), + "The descriptor for request part '" + descriptor.getName() + + "' must either have a description or be marked as " + + "ignored"); + } + this.descriptorsByName.put(descriptor.getName(), descriptor); + } + this.ignoreUndocumentedParts = ignoreUndocumentedParts; + } + + /** + * Returns a new {@code RequestPartsSnippet} configured with this snippet's attributes + * and its descriptors combined with the given {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestPartsSnippet and(RequestPartDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(this.descriptorsByName.values()); + combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return new RequestPartsSnippet(combinedDescriptors, this.getAttributes()); + } + + @Override + protected Map createModel(Operation operation) { + verifyRequestPartDescriptors(operation); + Map model = new HashMap<>(); + List> requestParts = new ArrayList<>(); + for (Entry entry : this.descriptorsByName + .entrySet()) { + RequestPartDescriptor descriptor = entry.getValue(); + if (!descriptor.isIgnored()) { + requestParts.add(createModelForDescriptor(descriptor)); + } + } + model.put("requestParts", requestParts); + return model; + } + + private void verifyRequestPartDescriptors(Operation operation) { + Set actualRequestParts = extractActualRequestParts(operation); + Set expectedRequestParts = new HashSet<>(); + for (Entry entry : this.descriptorsByName + .entrySet()) { + if (!entry.getValue().isOptional()) { + expectedRequestParts.add(entry.getKey()); + } + } + Set undocumentedRequestParts; + if (this.ignoreUndocumentedParts) { + undocumentedRequestParts = Collections.emptySet(); + } + else { + undocumentedRequestParts = new HashSet<>(actualRequestParts); + undocumentedRequestParts.removeAll(this.descriptorsByName.keySet()); + } + + Set missingRequestParts = new HashSet<>(expectedRequestParts); + missingRequestParts.removeAll(actualRequestParts); + + if (!undocumentedRequestParts.isEmpty() || !missingRequestParts.isEmpty()) { + verificationFailed(undocumentedRequestParts, missingRequestParts); + } + } + + private Set extractActualRequestParts(Operation operation) { + Set actualRequestParts = new HashSet<>(); + for (OperationRequestPart requestPart : operation.getRequest().getParts()) { + actualRequestParts.add(requestPart.getName()); + } + return actualRequestParts; + } + + private void verificationFailed(Set undocumentedRequestParts, + Set missingRequestParts) { + String message = ""; + if (!undocumentedRequestParts.isEmpty()) { + message += "Request parts with the following names were not documented: " + + undocumentedRequestParts; + } + if (!missingRequestParts.isEmpty()) { + if (message.length() > 0) { + message += ". "; + } + message += "Request parts with the following names were not found in " + + "the request: " + missingRequestParts; + } + throw new SnippetException(message); + } + + private Map createModelForDescriptor( + RequestPartDescriptor descriptor) { + Map model = new HashMap<>(); + model.put("name", descriptor.getName()); + model.put("description", descriptor.getDescription()); + model.put("optional", descriptor.isOptional()); + model.putAll(descriptor.getAttributes()); + return model; + } + +} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet new file mode 100644 index 000000000..3ed4773b5 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet @@ -0,0 +1,9 @@ +|=== +|Part|Description + +{{#requestParts}} +|`{{name}}` +|{{description}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parts.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parts.snippet new file mode 100644 index 000000000..b313f2d3e --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-parts.snippet @@ -0,0 +1,5 @@ +Part | Description +---- | ----------- +{{#requestParts}} +`{{name}}` | {{description}} +{{/requestParts}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java new file mode 100644 index 000000000..93de349a5 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2016 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 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.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; + +/** + * Tests for failures when rendering {@link RequestPartsSnippet} due to missing or + * undocumented request parts. + * + * @author Andy Wilkinson + */ +public class RequestPartsSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedPart() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo( + "Request parts with the following names were" + " not documented: [a]")); + new RequestPartsSnippet(Collections.emptyList()) + .document(new OperationBuilder("undocumented-part", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("a", "alpha".getBytes()).build()); + } + + @Test + public void missingPart() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Request parts with the following names were" + + " not found in the request: [a]")); + new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) + .document(new OperationBuilder("missing-part", + this.snippet.getOutputDirectory()).request("http://localhost") + .build()); + } + + @Test + public void undocumentedAndMissingParts() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Request parts with the following names were" + + " not documented: [b]. Request parts with the following" + + " names were not found in the request: [a]")); + new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) + .document(new OperationBuilder("undocumented-and-missing-parts", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("b", "bravo".getBytes()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java new file mode 100644 index 000000000..6ce2a09a2 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -0,0 +1,183 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.util.Arrays; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestPartsSnippet}. + * + * @author Andy Wilkinson + */ +public class RequestPartsSnippetTests extends AbstractSnippetTests { + + public RequestPartsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void requestParts() throws IOException { + this.snippet.expectRequestParts("request-parts") + .withContents(tableWithHeader("Part", "Description").row("`a`", "one") + .row("`b`", "two")); + new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"), + partWithName("b").description("two"))) + .document(operationBuilder("request-parts") + .request("http://localhost").part("a", "bravo".getBytes()) + .and().part("b", "bravo".getBytes()).build()); + } + + @Test + public void ignoredRequestPart() throws IOException { + this.snippet.expectRequestParts("ignored-request-part") + .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); + new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(), + partWithName("b").description("two"))) + .document(operationBuilder("ignored-request-part") + .request("http://localhost").part("a", "bravo".getBytes()) + .and().part("b", "bravo".getBytes()).build()); + } + + @Test + public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { + this.snippet.expectRequestParts("ignore-all-undocumented") + .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); + new RequestPartsSnippet(Arrays.asList(partWithName("b").description("two")), true) + .document(operationBuilder("ignore-all-undocumented") + .request("http://localhost").part("a", "bravo".getBytes()).and() + .part("b", "bravo".getBytes()).build()); + } + + @Test + public void missingOptionalRequestPart() throws IOException { + this.snippet.expectRequestParts("missing-optional-request-parts") + .withContents(tableWithHeader("Part", "Description").row("`a`", "one") + .row("`b`", "two")); + new RequestPartsSnippet( + Arrays.asList(partWithName("a").description("one").optional(), + partWithName("b").description("two"))).document( + operationBuilder("missing-optional-request-parts") + .request("http://localhost") + .part("b", "bravo".getBytes()).build()); + } + + @Test + public void presentOptionalRequestPart() throws IOException { + this.snippet.expectRequestParts("present-optional-request-part") + .withContents(tableWithHeader("Part", "Description").row("`a`", "one")); + new RequestPartsSnippet( + Arrays.asList(partWithName("a").description("one").optional())) + .document(operationBuilder("present-optional-request-part") + .request("http://localhost").part("a", "one".getBytes()) + .build()); + } + + @Test + public void requestPartsWithCustomAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-parts")) + .willReturn(snippetResource("request-parts-with-title")); + this.snippet.expectRequestParts("request-parts-with-custom-attributes") + .withContents(containsString("The title")); + + new RequestPartsSnippet( + Arrays.asList( + partWithName("a").description("one") + .attributes(key("foo").value("alpha")), + partWithName("b").description("two") + .attributes(key("foo").value("bravo"))), + attributes(key("title").value("The title"))) + .document(operationBuilder("request-parts-with-custom-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost").part("a", "alpha".getBytes()) + .and().part("b", "bravo".getBytes()).build()); + } + + @Test + public void requestPartsWithCustomDescriptorAttributes() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-parts")) + .willReturn(snippetResource("request-parts-with-extra-column")); + this.snippet.expectRequestParts("request-parts-with-custom-descriptor-attributes") + .withContents(tableWithHeader("Part", "Description", "Foo") + .row("a", "one", "alpha").row("b", "two", "bravo")); + + new RequestPartsSnippet(Arrays.asList( + partWithName("a").description("one") + .attributes(key("foo").value("alpha")), + partWithName("b").description("two") + .attributes(key("foo").value("bravo")))) + .document(operationBuilder( + "request-parts-with-custom-descriptor-attributes") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .part("a", "alpha".getBytes()).and() + .part("b", "bravo".getBytes()).build()); + } + + @Test + public void requestPartsWithOptionalColumn() throws IOException { + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-parts")) + .willReturn(snippetResource("request-parts-with-optional-column")); + this.snippet.expectRequestParts("request-parts-with-optional-column") + .withContents(tableWithHeader("Part", "Optional", "Description") + .row("a", "true", "one").row("b", "false", "two")); + + new RequestPartsSnippet( + Arrays.asList(partWithName("a").description("one").optional(), + partWithName("b").description("two"))).document( + operationBuilder("request-parts-with-optional-column") + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .request("http://localhost") + .part("a", "alpha".getBytes()).and() + .part("b", "bravo".getBytes()).build()); + } + + @Test + public void additionalDescriptors() throws IOException { + this.snippet.expectRequestParts("additional-descriptors") + .withContents(tableWithHeader("Part", "Description").row("`a`", "one") + .row("`b`", "two")); + RequestDocumentation.requestParts(partWithName("a").description("one")) + .and(partWithName("b").description("two")) + .document(operationBuilder("additional-descriptors") + .request("http://localhost").part("a", "bravo".getBytes()).and() + .part("b", "bravo".getBytes()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index 2799f1cc0..bc1c77856 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -126,6 +126,11 @@ public ExpectedSnippet expectPathParameters(String name) { return this; } + public ExpectedSnippet expectRequestParts(String name) { + expect(name, "request-parts"); + return this; + } + private ExpectedSnippet expect(String name, String type) { this.expectedName = name; this.expectedType = type; diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-extra-column.snippet new file mode 100644 index 000000000..95f52f2fd --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Part|Description|Foo + +{{#requestParts}} +|{{name}} +|{{description}} +|{{foo}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-optional-column.snippet new file mode 100644 index 000000000..ca0234649 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-optional-column.snippet @@ -0,0 +1,10 @@ +|=== +|Part|Optional|Description + +{{#requestParts}} +|{{name}} +|{{optional}} +|{{description}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-title.snippet new file mode 100644 index 000000000..7e6731826 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-parts-with-title.snippet @@ -0,0 +1,10 @@ +.{{title}} +|=== +|Part|Description + +{{#requestParts}} +|{{name}} +|{{description}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-extra-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-extra-column.snippet new file mode 100644 index 000000000..b81a83961 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-extra-column.snippet @@ -0,0 +1,5 @@ +Part | Description | Foo +---- | ----------- | --- +{{#requestParts}} +{{name}} | {{description}} | {{foo}} +{{/requestParts}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-optional-column.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-optional-column.snippet new file mode 100644 index 000000000..c92cdc893 --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-optional-column.snippet @@ -0,0 +1,5 @@ +Part | Optional | Description +---- | -------- | ----------- +{{#requestParts}} +{{name}} | {{optional}} | {{description}} +{{/requestParts}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-title.snippet b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-title.snippet new file mode 100644 index 000000000..0dc1b3e9f --- /dev/null +++ b/spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-parts-with-title.snippet @@ -0,0 +1,6 @@ +{{title}} +Part | Description +---- | ----------- +{{#requestParts}} +{{name}} | {{description}} +{{/requestParts}} \ No newline at end of file diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 28b76962b..5068d34fc 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -71,8 +71,10 @@ 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.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; @@ -81,6 +83,7 @@ import static org.springframework.restdocs.test.SnippetMatchers.httpRequest; import static org.springframework.restdocs.test.SnippetMatchers.httpResponse; import static org.springframework.restdocs.test.SnippetMatchers.snippet; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -259,6 +262,20 @@ public void requestFieldsSnippet() throws Exception { "request-fields.adoc"); } + @Test + public void requestPartsSnippet() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(fileUpload("/upload").file("foo", "bar".getBytes())) + .andExpect(status().isOk()).andDo(document("request-parts", requestParts( + partWithName("foo").description("The description")))); + + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/request-parts"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-parts.adoc"); + } + @Test public void responseFieldsSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -432,6 +449,15 @@ public void customContextPath() throws Exception { "$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'")))); } + @Test + public void multiPart() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + mockMvc.perform(fileUpload("/upload").file("test", "content".getBytes())) + .andExpect(status().isOk()).andDo(document("upload", + requestParts(partWithName("test").description("Foo")))); + } + private void assertExpectedSnippetFilesExist(File directory, String... snippets) { for (String snippet : snippets) { assertTrue(new File(directory, snippet).isFile()); @@ -473,6 +499,11 @@ public String bar() { return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; } + @RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public void upload() { + + } + } } diff --git a/spring-restdocs-mockmvc/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet b/spring-restdocs-mockmvc/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet new file mode 100644 index 000000000..e1db090ba --- /dev/null +++ b/spring-restdocs-mockmvc/src/test/resources/org/springframework/restdocs/templates/request-parts.snippet @@ -0,0 +1,9 @@ +|=== +|Request part|Description + +{{#requestParts}} +|`{{name}}` +|{{description}} + +{{/requestParts}} +|=== \ No newline at end of file diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 408d29059..658793ee7 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -64,8 +64,10 @@ 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.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris; @@ -185,6 +187,17 @@ public void requestFieldsSnippet() throws Exception { "http-response.adoc", "curl-request.adoc", "request-fields.adoc"); } + @Test + public void requestPartsSnippet() throws Exception { + given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("request-parts", + requestParts(partWithName("a").description("The description")))) + .multiPart("a", "foo").post("/upload").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/request-parts"), "http-request.adoc", + "http-response.adoc", "curl-request.adoc", "request-parts.adoc"); + } + @Test public void responseFieldsSnippet() throws Exception { given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) @@ -343,6 +356,11 @@ public String bar() { return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; } + @RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public void upload() { + + } + } } From b81c03646fd594e45d97ece21ca299056aabfb18 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 17 May 2016 17:15:01 +0100 Subject: [PATCH 115/898] Polish MustacheTemplate's javadoc --- .../restdocs/templates/mustache/MustacheTemplate.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java index 9b359edbd..cb98ab2a2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java @@ -31,9 +31,7 @@ public class MustacheTemplate implements Template { private final org.springframework.restdocs.mustache.Template delegate; /** - * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. When - * rendered, the given {@code defaultContext} will be combined with the render context - * prior to executing the delegate. + * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. * * @param delegate The delegate to adapt */ From 1c04ddc7e53c14943544800e9b3a11284ee578f2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 May 2016 15:38:49 +0100 Subject: [PATCH 116/898] Update contributing section, add link to Gitter chat room Closes gh-238 --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1488ceb0..18fd83473 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,12 @@ You will need Java 7 or later to build Spring REST Docs. It is built using [Grad ## Contributing Contributors to this project agree to uphold its [code of conduct][11]. -[Pull requests][12] are welcome. Please see the [contributor guidelines][13] for details. + +There are several that you can contribute to Spring REST Docs: + + - Open a [pull request][12]. Please see the [contributor guidelines][13] for details. + - Ask and answer questions on Stack Overflow using the [`spring-restdocs`][15] tag. + - Chat with fellow users [on Gitter][16]. ## Licence @@ -46,4 +51,5 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [12]: https://help.github.com/articles/using-pull-requests/ [13]: CONTRIBUTING.md [14]: http://www.apache.org/licenses/LICENSE-2.0.html - +[15]: http://stackoverflow.com/tags/spring-restdocs +[16]: https://gitter.im/spring-projects/spring-restdocs \ No newline at end of file From 117e8ec096b5e95b575bc8258245602a6145680a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 May 2016 15:48:50 +0100 Subject: [PATCH 117/898] Provide an API for configuring additional default snippets Previously, the default snippets could only be configured by replacing those that are configured out of the box. This commit adds a method, withAdditionalDefaults, that can be used to add one or more snippets to those that are configured out of the box. Closes gh-237 --- .../restdocs/config/SnippetConfigurer.java | 23 +++++++++++++++---- .../RestDocumentationConfigurerTests.java | 22 +++++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index cb72f152c..d6475aadc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.config; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -38,9 +39,9 @@ public abstract class SnippetConfigurer extends AbstractNestedConfigurer { - private List defaultSnippets = Arrays.asList(CliDocumentation.curlRequest(), - CliDocumentation.httpieRequest(), HttpDocumentation.httpRequest(), - HttpDocumentation.httpResponse()); + private List defaultSnippets = new ArrayList<>(Arrays.asList( + CliDocumentation.curlRequest(), CliDocumentation.httpieRequest(), + HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse())); /** * The default encoding for documentation snippets. @@ -97,10 +98,24 @@ public TYPE withEncoding(String encoding) { * * @param defaultSnippets the default snippets * @return {@code this} + * @see #withAdditionalDefaults(Snippet...) */ @SuppressWarnings("unchecked") public TYPE withDefaults(Snippet... defaultSnippets) { - this.defaultSnippets = Arrays.asList(defaultSnippets); + this.defaultSnippets = new ArrayList<>(Arrays.asList(defaultSnippets)); + return (TYPE) this; + } + + /** + * Configures additional documentation snippets that will be produced by default. + * + * @param additionalDefaultSnippets the additional default snippets + * @return {@code this} + * @see #withDefaults(Snippet...) + */ + @SuppressWarnings("unchecked") + public TYPE withAdditionalDefaults(Snippet... additionalDefaultSnippets) { + this.defaultSnippets.addAll(Arrays.asList(additionalDefaultSnippets)); return (TYPE) this; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index a629283f2..0872c20df 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -119,6 +119,26 @@ public void customDefaultSnippets() { assertThat(defaultSnippets, contains(instanceOf(CurlRequestSnippet.class))); } + @SuppressWarnings("unchecked") + @Test + public void additionalDefaultSnippets() { + Map configuration = new HashMap<>(); + Snippet snippet = mock(Snippet.class); + this.configurer.snippets().withAdditionalDefaults(snippet).apply(configuration, + createContext()); + assertThat(configuration, + hasEntry( + equalTo(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS), + instanceOf(List.class))); + List defaultSnippets = (List) configuration + .get(RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + assertThat(defaultSnippets, + contains(instanceOf(CurlRequestSnippet.class), + instanceOf(HttpieRequestSnippet.class), + instanceOf(HttpRequestSnippet.class), + instanceOf(HttpResponseSnippet.class), equalTo(snippet))); + } + @Test public void customSnippetEncoding() { Map configuration = new HashMap<>(); From 65e55ac15cf01a60bb07ef04bbab490d1f7a6b33 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 May 2016 16:10:47 +0100 Subject: [PATCH 118/898] Allow descriptors to be added using a List in addition to varags Closes gh-242 --- .../headers/RequestHeadersSnippet.java | 19 ++++++++--- .../headers/ResponseHeadersSnippet.java | 18 ++++++++-- .../restdocs/hypermedia/LinksSnippet.java | 19 ++++++++--- .../payload/RequestFieldsSnippet.java | 34 +++++++++++++++++-- .../payload/ResponseFieldsSnippet.java | 34 +++++++++++++++++-- .../request/PathParametersSnippet.java | 20 ++++++++--- .../request/RequestParametersSnippet.java | 17 ++++++++-- .../restdocs/request/RequestPartsSnippet.java | 20 ++++++++--- 8 files changed, 154 insertions(+), 27 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java index ecdc933a6..71ec61ab1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/RequestHeadersSnippet.java @@ -70,10 +70,21 @@ protected Set extractActualHeaders(Operation operation) { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(this.getHeaderDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + public final RequestHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestHeadersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestHeadersSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + this.getHeaderDescriptors()); + combinedDescriptors.addAll(additionalDescriptors); return new RequestHeadersSnippet(combinedDescriptors, getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java index 1a4171679..8b9ec45c3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/ResponseHeadersSnippet.java @@ -71,9 +71,21 @@ protected Set extractActualHeaders(Operation operation) { * @return the new snippet */ public final ResponseHeadersSnippet and(HeaderDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(this.getHeaderDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code ResponseHeadersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseHeadersSnippet and( + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + this.getHeaderDescriptors()); + combinedDescriptors.addAll(additionalDescriptors); return new ResponseHeadersSnippet(combinedDescriptors, getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index 51b28f927..f0a4cb30e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -249,10 +249,21 @@ protected Map createModelForDescriptor(LinkDescriptor descriptor * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public LinksSnippet and(LinkDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(this.descriptorsByRel.values()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + public final LinksSnippet and(LinkDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestHeadersSnippet} configured with this snippet's link + * extractor and attributes, and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final LinksSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + this.descriptorsByRel.values()); + combinedDescriptors.addAll(additionalDescriptors); return new LinksSnippet(this.linkExtractor, combinedDescriptors, getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index cfb2ee265..3d03d71a1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -107,7 +107,19 @@ protected byte[] getContent(Operation operation) throws IOException { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + public final RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestFieldsSnippet and(List additionalDescriptors) { return andWithPrefix("", additionalDescriptors); } @@ -121,7 +133,7 @@ public RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestFieldsSnippet andWithPrefix(String pathPrefix, + public final RequestFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); @@ -130,4 +142,22 @@ public RequestFieldsSnippet andWithPrefix(String pathPrefix, return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestFieldsSnippet andWithPrefix(String pathPrefix, + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getFieldDescriptors()); + combinedDescriptors.addAll(applyPathPrefix(pathPrefix, additionalDescriptors)); + return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 06b5ac655..f3ce35392 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -108,7 +108,19 @@ protected byte[] getContent(Operation operation) throws IOException { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + public final ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseFieldsSnippet and(List additionalDescriptors) { return andWithPrefix("", additionalDescriptors); } @@ -122,7 +134,7 @@ public ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public ResponseFieldsSnippet andWithPrefix(String pathPrefix, + public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); @@ -131,4 +143,22 @@ public ResponseFieldsSnippet andWithPrefix(String pathPrefix, return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } + /** + * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getFieldDescriptors()); + combinedDescriptors.addAll(applyPathPrefix(pathPrefix, additionalDescriptors)); + return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 509a604b7..afbd0b527 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -165,10 +165,22 @@ protected void verificationFailed(Set undocumentedParameters, * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public PathParametersSnippet and(ParameterDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(getParameterDescriptors().values()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + public final PathParametersSnippet and(ParameterDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code PathParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final PathParametersSnippet and( + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getParameterDescriptors().values()); + combinedDescriptors.addAll(additionalDescriptors); return new PathParametersSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java index 37a5696d6..1fa21bf7f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestParametersSnippet.java @@ -129,9 +129,20 @@ protected Set extractActualParameters(Operation operation) { * @return the new snippet */ public RequestParametersSnippet and(ParameterDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(getParameterDescriptors().values()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestParametersSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestParametersSnippet and(List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getParameterDescriptors().values()); + combinedDescriptors.addAll(additionalDescriptors); return new RequestParametersSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java index 1a5d30597..c55bca4e2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java @@ -115,14 +115,24 @@ protected RequestPartsSnippet(List descriptors, /** * Returns a new {@code RequestPartsSnippet} configured with this snippet's attributes * and its descriptors combined with the given {@code additionalDescriptors}. - * * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public RequestPartsSnippet and(RequestPartDescriptor... additionalDescriptors) { - List combinedDescriptors = new ArrayList<>(); - combinedDescriptors.addAll(this.descriptorsByName.values()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + public final RequestPartsSnippet and(RequestPartDescriptor... additionalDescriptors) { + return and(Arrays.asList(additionalDescriptors)); + } + + /** + * Returns a new {@code RequestPartsSnippet} configured with this snippet's attributes + * and its descriptors combined with the given {@code additionalDescriptors}. + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartsSnippet and( + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + this.descriptorsByName.values()); + combinedDescriptors.addAll(additionalDescriptors); return new RequestPartsSnippet(combinedDescriptors, this.getAttributes()); } From 404b99b0a4ea4dc1908b5d7d709db9e6e5a1f2b2 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 10:50:32 +0100 Subject: [PATCH 119/898] Avoid problems caused by | characters in table cell content Previously, a | character in a cell of an Asciidoctor table would break that table's formatting as it would be interpretted as a delimiter for the cell. The problem also affects Markdown tables where the | character is not within backticks. This commit addresses the problem with Asciidoctor tables by introducing a JMustache Lambda that escapes | characters by prefixing them with a backslash. Unfortunately, a complete solution to the problem when using Markdown is not as straightforward. Markdown only requires a | character to be espaced when it is not within backticks. Determing this isn't straightforward as the text would have to be parsed, taking into account the possibility of backticks themselves being escaped. Instead, this commit attempts to avoid the problem in a different way. The most likely source of a | character is when documenting a `@Pattern` constraint that uses a | character in its regular expression. To improve the readability of the regular expression this commit wraps it in backticks, thereby formatting it in a monospaced font in both Asciidoctor and Markdown and also avoiding any escaping problems with the latter. Closes gh-232 See gh-230 --- spring-restdocs-core/build.gradle | 1 + .../config/RestDocumentationConfigurer.java | 13 ++++- .../AsciidoctorTableCellContentLambda.java | 45 +++++++++++++++ .../templates/mustache/MustacheTemplate.java | 23 +++++++- .../mustache/MustacheTemplateEngine.java | 30 +++++++++- .../DefaultConstraintDescriptions.properties | 2 +- .../asciidoctor/default-links.snippet | 4 +- .../default-path-parameters.snippet | 4 +- .../default-request-fields.snippet | 6 +- .../default-request-headers.snippet | 4 +- .../default-request-parameters.snippet | 4 +- .../asciidoctor/default-request-parts.snippet | 4 +- .../default-response-fields.snippet | 6 +- .../default-response-headers.snippet | 4 +- .../RestDocumentationConfigurerTests.java | 30 ++++++++++ .../ConstraintDescriptionsTests.java | 2 +- ...dleConstraintDescriptionResolverTests.java | 2 +- .../headers/RequestHeadersSnippetTests.java | 20 +++++++ .../headers/ResponseHeadersSnippetTests.java | 19 +++++++ .../hypermedia/LinksSnippetTests.java | 19 +++++++ .../payload/RequestFieldsSnippetTests.java | 22 +++++++ .../payload/ResponseFieldsSnippetTests.java | 21 +++++++ .../request/PathParametersSnippetTests.java | 32 ++++++++++- .../RequestParametersSnippetTests.java | 20 +++++++ .../request/RequestPartsSnippetTests.java | 20 +++++++ ...sciidoctorTableCellContentLambdaTests.java | 57 +++++++++++++++++++ .../restdocs/test/OperationBuilder.java | 8 ++- 27 files changed, 393 insertions(+), 29 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 9f0934756..01ef4b9cc 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -38,6 +38,7 @@ dependencies { testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.hibernate:hibernate-validator' + testCompile 'org.springframework:spring-test' testRuntime 'org.glassfish:javax.el:3.0.0' } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index f77c7aa45..2b1216f4a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -17,15 +17,19 @@ package org.springframework.restdocs.config; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.mustache.Mustache; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; /** @@ -102,9 +106,16 @@ public void apply(Map configuration, if (engineToUse == null) { SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration .get(SnippetConfiguration.class.getName()); + Map templateContext = new HashMap<>(); + if (snippetConfiguration.getTemplateFormat().getId() + .equals(TemplateFormats.asciidoctor().getId())) { + templateContext.put("tableCellContent", + new AsciidoctorTableCellContentLambda()); + } engineToUse = new MustacheTemplateEngine( new StandardTemplateResourceResolver( - snippetConfiguration.getTemplateFormat())); + snippetConfiguration.getTemplateFormat()), + Mustache.compiler().escapeHTML(false), templateContext); } configuration.put(TemplateEngine.class.getName(), engineToUse); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java new file mode 100644 index 000000000..7881c6e08 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2016 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.templates.mustache; + +import java.io.IOException; +import java.io.Writer; + +import org.springframework.restdocs.mustache.Mustache.Lambda; +import org.springframework.restdocs.mustache.Template.Fragment; + +/** + * A {@link Lambda} that escapes {@code |} characters so that the do not break the table's + * formatting. + * + * @author Andy Wilkinson + */ +public final class AsciidoctorTableCellContentLambda implements Lambda { + + @Override + public void execute(Fragment fragment, Writer writer) throws IOException { + String output = fragment.execute(); + for (int i = 0; i < output.length(); i++) { + char current = output.charAt(i); + if (current == '|' && (i == 0 || output.charAt(i - 1) != '\\')) { + writer.append('\\'); + } + writer.append(current); + } + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java index cb98ab2a2..1d315360a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.templates.mustache; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.springframework.restdocs.templates.Template; @@ -30,18 +32,37 @@ public class MustacheTemplate implements Template { private final org.springframework.restdocs.mustache.Template delegate; + private final Map context; + /** * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. * * @param delegate The delegate to adapt */ public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate) { + this(delegate, Collections.emptyMap()); + } + + /** + * Creates a new {@code MustacheTemplate} that adapts the given {@code delegate}. + * During rendering, the given {@code context} and the context passed into + * {@link #render(Map)} will be combined and then passed to the delegate when it is + * {@link org.springframework.restdocs.mustache.Template#execute executed}. + * + * @param delegate The delegate to adapt + * @param context The context + */ + public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate, + Map context) { this.delegate = delegate; + this.context = context; } @Override public String render(Map context) { - return this.delegate.execute(context); + Map combinedContext = new HashMap<>(this.context); + combinedContext.putAll(context); + return this.delegate.execute(combinedContext); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java index 8079060ad..fadce5ae2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java @@ -18,6 +18,8 @@ import java.io.IOException; import java.io.InputStreamReader; +import java.util.Collections; +import java.util.Map; import org.springframework.core.io.Resource; import org.springframework.restdocs.mustache.Mustache; @@ -36,9 +38,11 @@ */ public class MustacheTemplateEngine implements TemplateEngine { + private final TemplateResourceResolver templateResourceResolver; + private final Compiler compiler; - private final TemplateResourceResolver templateResourceResolver; + private final Map context; /** * Creates a new {@code MustacheTemplateEngine} that will use the given @@ -60,16 +64,36 @@ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) */ public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, Compiler compiler) { + this(templateResourceResolver, compiler, Collections.emptyMap()); + } + + /** + * Creates a new {@code MustacheTemplateEngine} that will use the given + * {@code templateResourceResolver} to resolve templates and the given + * {@code compiler} to compile them. Compiled templates will be created with the given + * {@code context}. + * + * @param templateResourceResolver the resolver to use + * @param compiler the compiler to use + * @param context the context to pass to compiled templates + * @see MustacheTemplate#MustacheTemplate(org.springframework.restdocs.mustache.Template, + * Map) + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver, + Compiler compiler, Map context) { this.templateResourceResolver = templateResourceResolver; this.compiler = compiler; + this.context = context; } @Override public Template compileTemplate(String name) throws IOException { Resource templateResource = this.templateResourceResolver .resolveTemplateResource(name); - return new MustacheTemplate(this.compiler - .compile(new InputStreamReader(templateResource.getInputStream()))); + return new MustacheTemplate( + this.compiler.compile( + new InputStreamReader(templateResource.getInputStream())), + this.context); } /** diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties index 1cd5320f8..d42b5ae68 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/constraints/DefaultConstraintDescriptions.properties @@ -9,7 +9,7 @@ javax.validation.constraints.Min.description=Must be at least ${value} javax.validation.constraints.NotNull.description=Must not be null javax.validation.constraints.Null.description=Must be null javax.validation.constraints.Past.description=Must be in the past -javax.validation.constraints.Pattern.description=Must match the regular expression '${regexp}' +javax.validation.constraints.Pattern.description=Must match the regular expression `${regexp}` javax.validation.constraints.Size.description=Size must be between ${min} and ${max} inclusive org.hibernate.validator.constraints.CreditCardNumber.description=Must be a well-formed credit card number org.hibernate.validator.constraints.EAN.description=Must be a well-formed ${type} number diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet index 132643d8e..ab50823d7 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-links.snippet @@ -2,8 +2,8 @@ |Relation|Description {{#links}} -|`{{rel}}` -|{{description}} +|{{#tableCellContent}}`{{rel}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/links}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet index a6cc9f8d4..c318e4019 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-path-parameters.snippet @@ -3,8 +3,8 @@ |Parameter|Description {{#parameters}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/parameters}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet index 41e763e74..46cd43fe4 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-fields.snippet @@ -2,9 +2,9 @@ |Path|Type|Description {{#fields}} -|`{{path}}` -|`{{type}}` -|{{description}} +|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} +|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet index f3d660355..790a81bce 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-headers.snippet @@ -2,8 +2,8 @@ |Name|Description {{#headers}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/headers}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet index f338b345f..411c33c54 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parameters.snippet @@ -2,8 +2,8 @@ |Parameter|Description {{#parameters}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/parameters}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet index 3ed4773b5..06a65c1e2 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-parts.snippet @@ -2,8 +2,8 @@ |Part|Description {{#requestParts}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/requestParts}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet index 41e763e74..46cd43fe4 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-fields.snippet @@ -2,9 +2,9 @@ |Path|Type|Description {{#fields}} -|`{{path}}` -|`{{type}}` -|{{description}} +|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} +|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/fields}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet index f3d660355..790a81bce 100644 --- a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-headers.snippet @@ -2,8 +2,8 @@ |Name|Description {{#headers}} -|`{{name}}` -|{{description}} +|{{#tableCellContent}}`{{name}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} {{/headers}} |=== \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0872c20df..995a6ffb6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -36,7 +36,9 @@ import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; +import org.springframework.test.util.ReflectionTestUtils; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; @@ -164,6 +166,34 @@ public void customTemplateFormat() { is(equalTo(TemplateFormats.markdown()))); } + @SuppressWarnings("unchecked") + @Test + public void asciidoctorTableCellContentLambaIsInstalledWhenUsingAsciidoctorTemplateFormat() { + Map configuration = new HashMap<>(); + this.configurer.apply(configuration, createContext()); + TemplateEngine templateEngine = (TemplateEngine) configuration + .get(TemplateEngine.class.getName()); + MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine; + Map templateContext = (Map) ReflectionTestUtils + .getField(mustacheTemplateEngine, "context"); + assertThat(templateContext, hasEntry(equalTo("tableCellContent"), + instanceOf(AsciidoctorTableCellContentLambda.class))); + } + + @SuppressWarnings("unchecked") + @Test + public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidoctorTemplateFormat() { + Map configuration = new HashMap<>(); + this.configurer.snippetConfigurer.withTemplateFormat(TemplateFormats.markdown()); + this.configurer.apply(configuration, createContext()); + TemplateEngine templateEngine = (TemplateEngine) configuration + .get(TemplateEngine.class.getName()); + MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine; + Map templateContext = (Map) ReflectionTestUtils + .getField(mustacheTemplateEngine, "context"); + assertThat(templateContext.size(), equalTo(0)); + } + private RestDocumentationContext createContext() { ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( "build"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index a3dbd9abe..34e916c56 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -118,7 +118,7 @@ public void past() { @Test public void pattern() { assertThat(this.constraintDescriptions.descriptionsForProperty("pattern"), - contains("Must match the regular expression '[A-Z][a-z]+'")); + contains("Must match the regular expression `[A-Z][a-z]+`")); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java index f9a7f734e..ece62ab92 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolverTests.java @@ -137,7 +137,7 @@ public void defaultMessagePast() { @Test public void defaultMessagePattern() { assertThat(constraintDescriptionForField("pattern"), - is(equalTo("Must match the regular expression '[A-Z][a-z]+'"))); + is(equalTo("Must match the regular expression `[A-Z][a-z]+`"))); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 7a0c9edd0..5766661c5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -165,4 +166,23 @@ public void additionalDescriptors() throws IOException { .header("Connection", "keep-alive").build()); } + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + this.snippet.expectRequestHeaders("request-with-escaped-headers").withContents( + tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); + new RequestHeadersSnippet( + Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder("request-with-escaped-headers") + .request("http://localhost").header("Foo|Bar", "baz") + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index d7bf553ac..417259cba 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.OperationBuilder; @@ -155,4 +156,22 @@ public void additionalDescriptors() throws IOException { .build()); } + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + this.snippet.expectResponseHeaders("response-with-escaped-headers").withContents( + tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); + new ResponseHeadersSnippet( + Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) + .document(operationBuilder("response-with-escaped-headers") + .response().header("Foo|Bar", "baz").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 46f73b303..eb14248b7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -171,4 +172,22 @@ public void additionalDescriptors() throws IOException { .and(new LinkDescriptor("b").description("two")) .document(operationBuilder("additional-descriptors").build()); } + + @Test + public void tableCellContentIsEscapedWhenNecessary() throws IOException { + this.snippet.expectLinks("links-with-escaped-content") + .withContents(tableWithHeader("Relation", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")), + Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two"))) + .document(operationBuilder("links-with-escaped-content").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index eac0b8438..fa23f3467 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -26,6 +26,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -227,4 +228,25 @@ public void prefixedAdditionalDescriptors() throws IOException { .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } + @Test + public void requestWithFieldsWithEscapedContent() throws IOException { + this.snippet.expectRequestFields("request-fields-with-escaped-content") + .withContents(tableWithHeader("Path", "Type", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), + escapeIfNecessary("three|four"))); + + new RequestFieldsSnippet(Arrays.asList( + fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) + .document(operationBuilder("request-fields-with-escaped-content") + .request("http://localhost").content("{\"Foo|Bar\": 5}") + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 574899a09..db13d9424 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -26,6 +26,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -283,4 +284,24 @@ public void prefixedAdditionalDescriptors() throws IOException { .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } + @Test + public void responseWithFieldsWithEscapedContent() throws IOException { + this.snippet.expectResponseFields("response-fields-with-escaped-content") + .withContents(tableWithHeader("Path", "Type", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), + escapeIfNecessary("three|four"))); + + new ResponseFieldsSnippet(Arrays.asList( + fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) + .document(operationBuilder("response-fields-with-escaped-content") + .response().content("{\"Foo|Bar\": 5}").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index c5f700654..e09809918 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -192,9 +192,37 @@ public void additionalDescriptors() throws IOException { .build()); } + @Test + public void pathParametersWithEscapedContent() throws IOException { + this.snippet.expectPathParameters("path-parameters-with-escaped-content") + .withContents(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter", + "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); + + RequestDocumentation + .pathParameters(parameterWithName("Foo|Bar").description("one|two")) + .document(operationBuilder("path-parameters-with-escaped-content") + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "{Foo|Bar}") + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + private String getTitle() { - return this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}/{b}" - : "`/{a}/{b}`"; + return getTitle("/{a}/{b}"); + } + + private String getTitle(String title) { + if (this.templateFormat.equals(TemplateFormats.asciidoctor())) { + return title; + } + return "`" + title + "`"; } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 94135a3bf..78dac57dd 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -194,4 +195,23 @@ public void additionalDescriptors() throws IOException { .param("b", "bravo").build()); } + @Test + public void requestParametersWithEscapedContent() throws IOException { + this.snippet.expectRequestParameters("request-parameters-with-escaped-content") + .withContents(tableWithHeader("Parameter", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + + RequestDocumentation + .requestParameters(parameterWithName("Foo|Bar").description("one|two")) + .document(operationBuilder("request-parameters-with-escaped-content") + .request("http://localhost").param("Foo|Bar", "baz").build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java index 6ce2a09a2..6468d80b6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.AbstractSnippetTests; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -180,4 +181,23 @@ public void additionalDescriptors() throws IOException { .part("b", "bravo".getBytes()).build()); } + @Test + public void requestPartsWithEscapedContent() throws IOException { + this.snippet.expectRequestParts("request-parts-with-escaped-content") + .withContents(tableWithHeader("Part", "Description").row( + escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + + RequestDocumentation.requestParts(partWithName("Foo|Bar").description("one|two")) + .document(operationBuilder("request-parts-with-escaped-content") + .request("http://localhost").part("Foo|Bar", "baz".getBytes()) + .build()); + } + + private String escapeIfNecessary(String input) { + if (this.templateFormat.equals(TemplateFormats.markdown())) { + return input; + } + return input.replace("|", "\\|"); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java new file mode 100644 index 000000000..c9420919c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambdaTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2016 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.templates.mustache; + +import java.io.IOException; +import java.io.StringWriter; + +import org.junit.Test; + +import org.springframework.restdocs.mustache.Template.Fragment; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AsciidoctorTableCellContentLambda}. + * + * @author Andy Wilkinson + */ +public class AsciidoctorTableCellContentLambdaTests { + + @Test + public void verticalBarCharactersAreEscaped() throws IOException { + Fragment fragment = mock(Fragment.class); + given(fragment.execute()).willReturn("|foo|bar|baz|"); + StringWriter writer = new StringWriter(); + new AsciidoctorTableCellContentLambda().execute(fragment, writer); + assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|"))); + } + + @Test + public void escapedVerticalBarCharactersAreNotEscapedAgain() throws IOException { + Fragment fragment = mock(Fragment.class); + given(fragment.execute()).willReturn("\\|foo|bar\\|baz|"); + StringWriter writer = new StringWriter(); + new AsciidoctorTableCellContentLambda().execute(fragment, writer); + assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|"))); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 86b426a30..4e292b2eb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -29,6 +29,7 @@ import org.springframework.http.HttpStatus; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; +import org.springframework.restdocs.mustache.Mustache; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestFactory; @@ -45,6 +46,7 @@ import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; /** @@ -93,9 +95,13 @@ public OperationBuilder attribute(String name, Object value) { public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { + Map templateContext = new HashMap<>(); + templateContext.put("tableCellContent", + new AsciidoctorTableCellContentLambda()); this.attributes.put(TemplateEngine.class.getName(), new MustacheTemplateEngine( - new StandardTemplateResourceResolver(this.templateFormat))); + new StandardTemplateResourceResolver(this.templateFormat), + Mustache.compiler().escapeHTML(false), templateContext)); } RestDocumentationContext context = createContext(); this.attributes.put(RestDocumentationContext.class.getName(), context); From 7f02cdf72531e673d9ce425838b412adbe463dcf Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 11:04:30 +0100 Subject: [PATCH 120/898] Upgrade to JMustache 1.12 Closes gh-246 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e08c78a52..02b0977a5 100644 --- a/build.gradle +++ b/build.gradle @@ -61,7 +61,7 @@ subprojects { dependencies { dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6.1' dependency 'com.jayway.restassured:rest-assured:2.8.0' - dependency 'com.samskivert:jmustache:1.11' + dependency 'com.samskivert:jmustache:1.12' dependency 'commons-codec:commons-codec:1.10' dependency 'javax.servlet:javax.servlet-api:3.1.0' dependency 'javax.validation:validation-api:1.1.0.Final' From ad549ae7c0301ea39c6e67dcbd734e943e4f90cc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 11:30:49 +0100 Subject: [PATCH 121/898] Ensure that additional snippets are only called once Previously, when additional snippets were added to a RestDocumentationResultHandler, those snippets would then be called for every subsequent call to handle(MvcResult). This is problematic as the snippets and their configuration are specific to a particular MvcResult that is being documented. This commit updates RestDocumentationResultHandler so that it clears its additional snippets each time handle(MvcResult) is called. Closes gh-243 --- .../RestDocumentationResultHandler.java | 7 +- .../RestDocumentationResultHandlerTests.java | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandlerTests.java diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 962f12b47..1969e5e94 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -44,6 +44,8 @@ */ public class RestDocumentationResultHandler implements ResultHandler { + private final List additionalSnippets; + private final String identifier; private final OperationRequestPreprocessor requestPreprocessor; @@ -80,6 +82,7 @@ public class RestDocumentationResultHandler implements ResultHandler { this.requestPreprocessor = requestPreprocessor; this.responsePreprocessor = responsePreprocessor; this.snippets = new ArrayList<>(Arrays.asList(snippets)); + this.additionalSnippets = new ArrayList<>(); } @Override @@ -110,7 +113,7 @@ public void handle(MvcResult result) throws Exception { * @return this {@code ResultDocumentationResultHandler} */ public RestDocumentationResultHandler snippets(Snippet... snippets) { - this.snippets.addAll(Arrays.asList(snippets)); + this.additionalSnippets.addAll(Arrays.asList(snippets)); return this; } @@ -120,6 +123,8 @@ private List getSnippets(MvcResult result) { (List) result.getRequest().getAttribute( "org.springframework.restdocs.mockmvc.defaultSnippets")); combinedSnippets.addAll(this.snippets); + combinedSnippets.addAll(this.additionalSnippets); + this.additionalSnippets.clear(); return combinedSnippets; } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandlerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandlerTests.java new file mode 100644 index 000000000..4c8b53ded --- /dev/null +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandlerTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2014-2016 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.mockmvc; + +import java.util.Arrays; + +import org.junit.Test; +import org.mockito.Matchers; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.test.web.servlet.MvcResult; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link RestDocumentationResultHandler}. + * + * @author Andy Wilkinson + */ +public class RestDocumentationResultHandlerTests { + + @Test + public void additionalSnippetsAreOnlyCalledOnce() throws Exception { + Snippet snippet = mock(Snippet.class); + RestDocumentationResultHandler resultHandler = new RestDocumentationResultHandler( + "test", snippet); + Snippet defaultSnippet = mock(Snippet.class); + Snippet additionalSnippet = mock(Snippet.class); + resultHandler.snippets(additionalSnippet); + MvcResult result = mock(MvcResult.class); + MockHttpServletRequest request = new MockHttpServletRequest("GET", + "http://localhost"); + request.setAttribute("org.springframework.restdocs.mockmvc.defaultSnippets", + Arrays.asList(defaultSnippet)); + given(result.getRequest()).willReturn(request); + given(result.getResponse()).willReturn(new MockHttpServletResponse()); + resultHandler.handle(result); + resultHandler.handle(result); + verify(defaultSnippet, times(2)).document(Matchers.any(Operation.class)); + verify(snippet, times(2)).document(Matchers.any(Operation.class)); + verify(additionalSnippet, times(1)).document(Matchers.any(Operation.class)); + } + +} From 09c9cac857e07bf33c6894bfaff02fb07f45dc70 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Sat, 21 May 2016 12:34:23 +0900 Subject: [PATCH 122/898] Polish --- docs/src/docs/asciidoc/getting-started.adoc | 4 ++-- docs/src/test/java/com/example/mockmvc/Payload.java | 2 +- .../test/java/com/example/restassured/Payload.java | 11 +++++------ .../preprocess/OperationPreprocessorAdapter.java | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index eb9c1fe5f..094c64c29 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -171,7 +171,7 @@ static content] by Spring Boot. To do so, configure your project's build so that 1. The documentation is generated before the jar is built 2. The generated documentation is included in the jar -[source,xml,indent=0,role="primary",role="primary"] +[source,xml,indent=0,role="primary"] .Maven ---- <1> @@ -253,7 +253,7 @@ When using JUnit, the first step in generating documentation snippets is to decl snippets should be written. This output directory should match the snippets directory that you have configured in your `build.gradle` or `pom.xml` file. -For Maven (`pom.xml` that will typically be `target/generated-snippets` and for +For Maven (`pom.xml`) that will typically be `target/generated-snippets` and for Gradle (`build.gradle`) it will typically be `build/generated-snippets`: [source,java,indent=0,role="primary"] diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index b811c1a6c..615354a3f 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -80,7 +80,7 @@ public void descriptorReuse() throws Exception { this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(document("book", responseFields(book))); // <1> - // end::single-book[] + // end::single-book[] // tag::book-array[] this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)) diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index 225eb6194..155e357e5 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -68,9 +68,9 @@ public void constraints() throws Exception { fieldWithPath("email").description("The user's email address") .attributes(key("constraints") .value("Must be a valid email address"))))) // <3> - // end::constraints[] - .when().post("/users") - .then().assertThat().statusCode(is(200)); + // end::constraints[] + .when().post("/users") + .then().assertThat().statusCode(is(200)); } public void descriptorReuse() throws Exception { @@ -90,10 +90,9 @@ public void descriptorReuse() throws Exception { .filter(document("books", responseFields( fieldWithPath("[]").description("An array of books")) // <1> .andWithPrefix("[].", book))) // <2> - .when().get("/books/1") - .then().assertThat().statusCode(is(200)); + .when().get("/books/1") + .then().assertThat().statusCode(is(200)); // end::book-array[] - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java index f2654c1bf..544de0f45 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java @@ -21,7 +21,7 @@ /** * An implementation of {@link OperationPreprocessor} that returns the request and - * response as-is. To be subclasses by preprocessor implementations that only modify the + * response as-is. To be subclassed by preprocessor implementations that only modify the * request or the response. * * @author Andy Wilkinson From d58f69175fc00dace8c999c71b0457b0c0e67367 Mon Sep 17 00:00:00 2001 From: Marcel Overdijk Date: Mon, 23 May 2016 20:37:12 +0200 Subject: [PATCH 123/898] Provide utility method for applying a path prefix to field descriptors applyPathPrefix is generally useful but, previously, it was only available as a protected method on AbstractFieldsSnippet. This commit moves the method from AbstractFieldsSnippet to PayloadDocumentation where it's now public and static. Closes gh-245 --- .../payload/AbstractFieldsSnippet.java | 26 ----------------- .../payload/PayloadDocumentation.java | 29 +++++++++++++++++++ .../payload/RequestFieldsSnippet.java | 5 ++-- .../payload/ResponseFieldsSnippet.java | 5 ++-- 4 files changed, 35 insertions(+), 30 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index c64507128..fcc67c360 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -204,30 +204,4 @@ protected Map createModelForDescriptor(FieldDescriptor descripto return model; } - /** - * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} - * applied to their paths. - * - * @param pathPrefix the path prefix - * @param descriptors the descriptors to copy - * @return the copied descriptors with the prefix applied - */ - protected final List applyPathPrefix(String pathPrefix, - List descriptors) { - List prefixedDescriptors = new ArrayList<>(); - for (FieldDescriptor descriptor : descriptors) { - FieldDescriptor prefixedDescriptor = new FieldDescriptor( - pathPrefix + descriptor.getPath()) - .description(descriptor.getDescription()) - .type(descriptor.getType()); - if (descriptor.isIgnored()) { - prefixedDescriptor.ignored(); - } - if (descriptor.isOptional()) { - prefixedDescriptor.optional(); - } - prefixedDescriptors.add(prefixedDescriptor); - } - return prefixedDescriptors; - } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index bb94c960d..bfce18790 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -16,7 +16,9 @@ package org.springframework.restdocs.payload; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -264,4 +266,31 @@ public static ResponseFieldsSnippet relaxedResponseFields( return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} + * applied to their paths. + * + * @param pathPrefix the path prefix + * @param descriptors the descriptors to copy + * @return the copied descriptors with the prefix applied + */ + public static List applyPathPrefix(String pathPrefix, + List descriptors) { + List prefixedDescriptors = new ArrayList<>(); + for (FieldDescriptor descriptor : descriptors) { + FieldDescriptor prefixedDescriptor = new FieldDescriptor( + pathPrefix + descriptor.getPath()) + .description(descriptor.getDescription()) + .type(descriptor.getType()); + if (descriptor.isIgnored()) { + prefixedDescriptor.ignored(); + } + if (descriptor.isOptional()) { + prefixedDescriptor.optional(); + } + prefixedDescriptors.add(prefixedDescriptor); + } + return prefixedDescriptors; + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index 3d03d71a1..ea8f68b2c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -138,7 +138,7 @@ public final RequestFieldsSnippet andWithPrefix(String pathPrefix, List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); combinedDescriptors.addAll( - applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } @@ -156,7 +156,8 @@ public final RequestFieldsSnippet andWithPrefix(String pathPrefix, List additionalDescriptors) { List combinedDescriptors = new ArrayList<>( getFieldDescriptors()); - combinedDescriptors.addAll(applyPathPrefix(pathPrefix, additionalDescriptors)); + combinedDescriptors.addAll( + PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index f3ce35392..cabe29484 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -139,7 +139,7 @@ public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); combinedDescriptors.addAll( - applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } @@ -157,7 +157,8 @@ public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, List additionalDescriptors) { List combinedDescriptors = new ArrayList<>( getFieldDescriptors()); - combinedDescriptors.addAll(applyPathPrefix(pathPrefix, additionalDescriptors)); + combinedDescriptors.addAll( + PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } From 5edc27fc990b984c733d88850f9c971529846da8 Mon Sep 17 00:00:00 2001 From: Marcel Overdijk Date: Tue, 24 May 2016 11:42:35 +0200 Subject: [PATCH 124/898] Allow descriptors to be provided as a List as well as via varags Closes gh-247 --- .../restdocs/headers/HeaderDocumentation.java | 69 +++++ .../hypermedia/HypermediaDocumentation.java | 195 ++++++++++++++ .../payload/PayloadDocumentation.java | 167 ++++++++++++ .../request/RequestDocumentation.java | 244 ++++++++++++++++++ 4 files changed, 675 insertions(+) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java index 1eb4f0f9e..740f6a78b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.headers; import java.util.Arrays; +import java.util.List; import java.util.Map; import org.springframework.restdocs.snippet.Snippet; @@ -59,6 +60,21 @@ public static RequestHeadersSnippet requestHeaders(HeaderDescriptor... descripto return new RequestHeadersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a new {@link Snippet} that will document the headers of the API operation's + * request. The headers will be documented using the given {@code descriptors}. + *

          + * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. + * + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers + * @see #headerWithName(String) + */ + public static RequestHeadersSnippet requestHeaders(List descriptors) { + return new RequestHeadersSnippet(descriptors); + } + /** * Returns a new {@link Snippet} that will document the headers of the API * operations's request. The given {@code attributes} will be available during snippet @@ -77,6 +93,24 @@ public static RequestHeadersSnippet requestHeaders(Map attribute return new RequestHeadersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@link Snippet} that will document the headers of the API + * operations's request. The given {@code attributes} will be available during snippet + * generation and the headers will be documented using the given {@code descriptors}. + *

          + * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's headers + * @return the snippet that will document the request headers + * @see #headerWithName(String) + */ + public static RequestHeadersSnippet requestHeaders(Map attributes, + List descriptors) { + return new RequestHeadersSnippet(descriptors, attributes); + } + /** * Returns a new {@link Snippet} that will document the headers of the API operation's * response. The headers will be documented using the given {@code descriptors}. @@ -93,6 +127,22 @@ public static ResponseHeadersSnippet responseHeaders( return new ResponseHeadersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a new {@link Snippet} that will document the headers of the API operation's + * response. The headers will be documented using the given {@code descriptors}. + *

          + * If a header is documented, is not marked as optional, and is not present in the + * request, a failure will occur. + * + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers + * @see #headerWithName(String) + */ + public static ResponseHeadersSnippet responseHeaders( + List descriptors) { + return new ResponseHeadersSnippet(descriptors); + } + /** * Returns a new {@link Snippet} that will document the headers of the API * operations's response. The given {@code attributes} will be available during @@ -112,4 +162,23 @@ public static ResponseHeadersSnippet responseHeaders(Map attribu return new ResponseHeadersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@link Snippet} that will document the headers of the API + * operations's response. The given {@code attributes} will be available during + * snippet generation and the headers will be documented using the given + * {@code descriptors}. + *

          + * If a header is documented, is not marked as optional, and is not present in the + * response, a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response's headers + * @return the snippet that will document the response headers + * @see #headerWithName(String) + */ + public static ResponseHeadersSnippet responseHeaders(Map attributes, + List descriptors) { + return new ResponseHeadersSnippet(descriptors, attributes); + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index 95f70b3be..a6098ce52 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.hypermedia; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -66,6 +67,31 @@ public static LinksSnippet links(LinkDescriptor... descriptors) { Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response automatically based on its + * content type and will be documented using the given {@code descriptors}. + *

          + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. + *

          + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet links(List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. Links will be extracted from the response automatically based on its @@ -86,6 +112,25 @@ public static LinksSnippet relaxedLinks(LinkDescriptor... descriptors) { Arrays.asList(descriptors), true); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response automatically based on its + * content type and will be documented using the given {@code descriptors}. + *

          + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, true); + } + /** * Returns a new {@code Snippet} that will document the links in the API call's * response. The given {@code attributes} will be available during snippet generation. @@ -115,6 +160,34 @@ public static LinksSnippet links(Map attributes, Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document 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 + * and will be documented using the given {@code descriptors}. + *

          + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. + *

          + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet links(Map attributes, + List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes); + } + /** * Returns a new {@code Snippet} that will document the links in the API call's * response. The given {@code attributes} will be available during snippet generation. @@ -138,6 +211,28 @@ public static LinksSnippet relaxedLinks(Map attributes, Arrays.asList(descriptors), attributes, true); } + /** + * Returns a new {@code Snippet} that will document 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 + * and will be documented using the given {@code descriptors}. + *

          + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(Map attributes, + List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes, true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. Links will be extracted from the response using the given @@ -165,6 +260,33 @@ public static LinksSnippet links(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response using the given + * {@code linkExtractor} and will be documented using the given {@code descriptors}. + *

          + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. + *

          + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet links(LinkExtractor linkExtractor, + List descriptors) { + return new LinksSnippet(linkExtractor, descriptors); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. Links will be extracted from the response using the given @@ -186,6 +308,27 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), true); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. Links will be extracted from the response using the given + * {@code linkExtractor} and will be documented using the given {@code descriptors}. + *

          + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + List descriptors) { + return new LinksSnippet(linkExtractor, descriptors, true); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. The given {@code attributes} will be available during snippet generation. @@ -215,6 +358,35 @@ public static LinksSnippet links(LinkExtractor linkExtractor, return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response using the given {@code linkExtractor} and + * will be documented using the given {@code descriptors}. + *

          + * If a link is present in the response, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a link + * is documented, is not marked as optional, and is not present in the response, a + * failure will also occur. + *

          + * If you do not want to document a link, a link descriptor can be marked as + * {@link LinkDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet links(LinkExtractor linkExtractor, + Map attributes, List descriptors) { + return new LinksSnippet(linkExtractor, descriptors, attributes); + } + /** * Returns a new {@code Snippet} that will document the links in the API operation's * response. The given {@code attributes} will be available during snippet generation. @@ -239,6 +411,29 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, true); } + /** + * Returns a new {@code Snippet} that will document the links in the API operation's + * response. The given {@code attributes} will be available during snippet generation. + * Links will be extracted from the response using the given {@code linkExtractor} and + * will be documented using the given {@code descriptors}. + *

          + * If a link is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented links will be ignored. + *

          + * If a descriptor does not have a {@link LinkDescriptor#description(Object) + * description}, the {@link Link#getTitle() title} of the link will be used. If the + * link does not have a title a failure will occur. + * + * @param attributes the attributes + * @param linkExtractor used to extract the links from the response + * @param descriptors the descriptions of the response's links + * @return the snippet that will document the links + */ + public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, + Map attributes, List descriptors) { + return new LinksSnippet(linkExtractor, descriptors, attributes, true); + } + /** * Returns a {@code LinkExtractor} capable of extracting links in Hypermedia * Application Language (HAL) format where the links are found in a map named diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index bfce18790..464b7e2f1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -121,6 +121,29 @@ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) return new RequestFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operations's + * request payload. The fields will be documented using the given {@code descriptors}. + *

          + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet requestFields(List descriptors) { + return new RequestFieldsSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the fields of the API operations's * request payload. The fields will be documented using the given {@code descriptors}. @@ -137,6 +160,22 @@ public static RequestFieldsSnippet relaxedRequestFields( return new RequestFieldsSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the fields of the API operations's + * request payload. The fields will be documented using the given {@code descriptors}. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + List descriptors) { + return new RequestFieldsSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * request payload. The fields will be documented using the given {@code descriptors} @@ -163,6 +202,32 @@ public static RequestFieldsSnippet requestFields(Map attributes, return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * request payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

          + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * payload, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet requestFields(Map attributes, + List descriptors) { + return new RequestFieldsSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * request payload. The fields will be documented using the given {@code descriptors} @@ -181,6 +246,24 @@ public static RequestFieldsSnippet relaxedRequestFields( return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * request payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + Map attributes, List descriptors) { + return new RequestFieldsSnippet(descriptors, attributes, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -205,6 +288,30 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor return new ResponseFieldsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

          + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet responseFields(List descriptors) { + return new ResponseFieldsSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -222,6 +329,23 @@ public static ResponseFieldsSnippet relaxedResponseFields( return new ResponseFieldsSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * . + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + List descriptors) { + return new ResponseFieldsSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -248,6 +372,32 @@ public static ResponseFieldsSnippet responseFields(Map attribute return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

          + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet responseFields(Map attributes, + List descriptors) { + return new ResponseFieldsSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -266,6 +416,23 @@ public static ResponseFieldsSnippet relaxedResponseFields( return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes, true); } + /** Returns a {@code Snippet} that will document the fields of the API operation's + * response payload. The fields will be documented using the given {@code descriptors} + * and the given {@code attributes} will be available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(descriptors, attributes, true); + } + /** * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} * applied to their paths. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index 4fec764af..c343cd7c7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.request; import java.util.Arrays; +import java.util.List; import java.util.Map; import org.springframework.restdocs.operation.OperationRequest; @@ -76,6 +77,28 @@ public static PathParametersSnippet pathParameters( return new PathParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

          + * If a parameter is present in the request path, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request path, a failure will also occur. + *

          + * If you do not want to document a path parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. + * + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet pathParameters( + List descriptors) { + return new PathParametersSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The parameters will be documented using the given @@ -92,6 +115,22 @@ public static PathParametersSnippet relaxedPathParameters( return new PathParametersSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

          + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + List descriptors) { + return new PathParametersSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -116,6 +155,30 @@ public static PathParametersSnippet pathParameters(Map attribute return new PathParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

          + * If a parameter is present in the request path, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request path, a failure will also occur. + *

          + * If you do not want to document a path parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet pathParameters(Map attributes, + List descriptors) { + return new PathParametersSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the path parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -134,6 +197,24 @@ public static PathParametersSnippet relaxedPathParameters( return new PathParametersSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the path parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

          + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the parameters in the request's path + * @return the snippet that will document the parameters + */ + public static PathParametersSnippet relaxedPathParameters( + Map attributes, List descriptors) { + return new PathParametersSnippet(descriptors, attributes, true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The parameters will be documented using the given @@ -157,6 +238,29 @@ public static RequestParametersSnippet requestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

          + * If a parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

          + * If you do not want to document a request parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. + * + * @param descriptors The descriptions of the request's parameters + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet requestParameters( + List descriptors) { + return new RequestParametersSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The parameters will be documented using the given @@ -174,6 +278,23 @@ public static RequestParametersSnippet relaxedRequestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The parameters will be documented using the given + * {@code descriptors}. + *

          + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param descriptors The descriptions of the request's parameters + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + List descriptors) { + return new RequestParametersSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -199,6 +320,31 @@ public static RequestParametersSnippet requestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

          + * If a parameter is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * parameter is documented, is not marked as optional, and is not present in the + * request, a failure will also occur. + *

          + * If you do not want to document a request parameter, a parameter descriptor can be + * marked as {@link ParameterDescriptor#ignored}. This will prevent it from appearing + * in the generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parameters + * @return the snippet that will document the parameters + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet requestParameters( + Map attributes, List descriptors) { + return new RequestParametersSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the parameters from the API * operation's request. The given {@code attributes} will be available during snippet @@ -218,6 +364,25 @@ public static RequestParametersSnippet relaxedRequestParameters( return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the parameters from the API + * operation's request. The given {@code attributes} will be available during snippet + * rendering and the parameters will be documented using the given {@code descriptors} + * . + *

          + * If a parameter is documented, is not marked as optional, and is not present in the + * response, a failure will occur. Any undocumented parameters will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parameters + * @return the snippet that will document the parameters + * @see OperationRequest#getParameters() + */ + public static RequestParametersSnippet relaxedRequestParameters( + Map attributes, List descriptors) { + return new RequestParametersSnippet(descriptors, attributes, true); + } + /** * Returns a {@code Snippet} that will document the parts from the API operation's * request. The parts will be documented using the given {@code descriptors}. @@ -239,6 +404,27 @@ public static RequestPartsSnippet requestParts(RequestPartDescriptor... descript return new RequestPartsSnippet(Arrays.asList(descriptors)); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The parts will be documented using the given {@code descriptors}. + *

          + * If a part is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part + * is documented, is not marked as optional, and is not present in the request, a + * failure will also occur. + *

          + * If you do not want to document a part, a part descriptor can be marked as + * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param descriptors The descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet requestParts(List descriptors) { + return new RequestPartsSnippet(descriptors); + } + /** * Returns a {@code Snippet} that will document the parts from the API operation's * request. The parameters will be documented using the given {@code descriptors}. @@ -255,6 +441,22 @@ public static RequestPartsSnippet relaxedRequestParts( return new RequestPartsSnippet(Arrays.asList(descriptors), true); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The parameters will be documented using the given {@code descriptors}. + *

          + * If a part is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented parts will be ignored. + * + * @param descriptors The descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet relaxedRequestParts( + List descriptors) { + return new RequestPartsSnippet(descriptors, true); + } + /** * Returns a {@code Snippet} that will document the parts from the API operation's * request. The given {@code attributes} will be available during snippet rendering @@ -279,6 +481,30 @@ public static RequestPartsSnippet requestParts(Map attributes, return new RequestPartsSnippet(Arrays.asList(descriptors), attributes); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The given {@code attributes} will be available during snippet rendering + * and the parts will be documented using the given {@code descriptors}. + *

          + * If a part is present in the request, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part + * is documented, is not marked as optional, and is not present in the request, a + * failure will also occur. + *

          + * If you do not want to document a part, a part descriptor can be marked as + * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParts() + */ + public static RequestPartsSnippet requestParts(Map attributes, + List descriptors) { + return new RequestPartsSnippet(descriptors, attributes); + } + /** * Returns a {@code Snippet} that will document the parts from the API operation's * request. The given {@code attributes} will be available during snippet rendering @@ -297,4 +523,22 @@ public static RequestPartsSnippet relaxedRequestParts(Map attrib return new RequestPartsSnippet(Arrays.asList(descriptors), attributes, true); } + /** + * Returns a {@code Snippet} that will document the parts from the API operation's + * request. The given {@code attributes} will be available during snippet rendering + * and the parts will be documented using the given {@code descriptors}. + *

          + * If a part is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented parts will be ignored. + * + * @param attributes the attributes + * @param descriptors the descriptions of the request's parts + * @return the snippet + * @see OperationRequest#getParameters() + */ + public static RequestPartsSnippet relaxedRequestParts(Map attributes, + List descriptors) { + return new RequestPartsSnippet(descriptors, attributes, true); + } + } From 81b2f11ebfa223cdb2b27dd1ea39cabdbecd9a1a Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 13:54:17 +0100 Subject: [PATCH 125/898] Polish "Allow descriptors to be provided as a List as well as via varags" See gh-247 --- .../restdocs/headers/HeaderDocumentation.java | 12 ++++--- .../hypermedia/HypermediaDocumentation.java | 31 +++++++++---------- .../payload/PayloadDocumentation.java | 27 +++++++++------- .../request/RequestDocumentation.java | 28 +++++++++-------- 4 files changed, 51 insertions(+), 47 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java index 740f6a78b..9f32638e1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/headers/HeaderDocumentation.java @@ -27,6 +27,7 @@ * * @author Andreas Evers * @author Andy Wilkinson + * @author Marcel Overdijk */ public abstract class HeaderDocumentation { @@ -57,7 +58,7 @@ public static HeaderDescriptor headerWithName(String name) { * @see #headerWithName(String) */ public static RequestHeadersSnippet requestHeaders(HeaderDescriptor... descriptors) { - return new RequestHeadersSnippet(Arrays.asList(descriptors)); + return requestHeaders(Arrays.asList(descriptors)); } /** @@ -71,7 +72,8 @@ public static RequestHeadersSnippet requestHeaders(HeaderDescriptor... descripto * @return the snippet that will document the request headers * @see #headerWithName(String) */ - public static RequestHeadersSnippet requestHeaders(List descriptors) { + public static RequestHeadersSnippet requestHeaders( + List descriptors) { return new RequestHeadersSnippet(descriptors); } @@ -90,7 +92,7 @@ public static RequestHeadersSnippet requestHeaders(List descri */ public static RequestHeadersSnippet requestHeaders(Map attributes, HeaderDescriptor... descriptors) { - return new RequestHeadersSnippet(Arrays.asList(descriptors), attributes); + return requestHeaders(attributes, Arrays.asList(descriptors)); } /** @@ -124,7 +126,7 @@ public static RequestHeadersSnippet requestHeaders(Map attribute */ public static ResponseHeadersSnippet responseHeaders( HeaderDescriptor... descriptors) { - return new ResponseHeadersSnippet(Arrays.asList(descriptors)); + return responseHeaders(Arrays.asList(descriptors)); } /** @@ -159,7 +161,7 @@ public static ResponseHeadersSnippet responseHeaders( */ public static ResponseHeadersSnippet responseHeaders(Map attributes, HeaderDescriptor... descriptors) { - return new ResponseHeadersSnippet(Arrays.asList(descriptors), attributes); + return responseHeaders(attributes, Arrays.asList(descriptors)); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java index a6098ce52..191ffea35 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/HypermediaDocumentation.java @@ -24,6 +24,7 @@ * Static factory methods for documenting a RESTful API that utilizes Hypermedia. * * @author Andy Wilkinson + * @author Marcel Overdijk */ public abstract class HypermediaDocumentation { @@ -63,8 +64,7 @@ public static LinkDescriptor linkWithRel(String rel) { * @return the snippet that will document the links */ public static LinksSnippet links(LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), - Arrays.asList(descriptors)); + return links(Arrays.asList(descriptors)); } /** @@ -108,8 +108,7 @@ public static LinksSnippet links(List descriptors) { * @return the snippet that will document the links */ public static LinksSnippet relaxedLinks(LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), - Arrays.asList(descriptors), true); + return relaxedLinks(Arrays.asList(descriptors)); } /** @@ -156,8 +155,7 @@ public static LinksSnippet relaxedLinks(List descriptors) { */ public static LinksSnippet links(Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), - Arrays.asList(descriptors), attributes); + return links(attributes, Arrays.asList(descriptors)); } /** @@ -207,8 +205,7 @@ public static LinksSnippet links(Map attributes, */ public static LinksSnippet relaxedLinks(Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), - Arrays.asList(descriptors), attributes, true); + return relaxedLinks(attributes, Arrays.asList(descriptors)); } /** @@ -229,8 +226,9 @@ public static LinksSnippet relaxedLinks(Map attributes, * @return the snippet that will document the links */ public static LinksSnippet relaxedLinks(Map attributes, - List descriptors) { - return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes, true); + List descriptors) { + return new LinksSnippet(new ContentTypeLinkExtractor(), descriptors, attributes, + true); } /** @@ -257,7 +255,7 @@ public static LinksSnippet relaxedLinks(Map attributes, */ public static LinksSnippet links(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, Arrays.asList(descriptors)); + return links(linkExtractor, Arrays.asList(descriptors)); } /** @@ -305,7 +303,7 @@ public static LinksSnippet links(LinkExtractor linkExtractor, */ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), true); + return relaxedLinks(linkExtractor, Arrays.asList(descriptors)); } /** @@ -325,7 +323,7 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, * @return the snippet that will document the links */ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, - List descriptors) { + List descriptors) { return new LinksSnippet(linkExtractor, descriptors, true); } @@ -355,7 +353,7 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, */ public static LinksSnippet links(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes); + return links(linkExtractor, attributes, Arrays.asList(descriptors)); } /** @@ -407,8 +405,7 @@ public static LinksSnippet links(LinkExtractor linkExtractor, */ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, Map attributes, LinkDescriptor... descriptors) { - return new LinksSnippet(linkExtractor, Arrays.asList(descriptors), attributes, - true); + return relaxedLinks(linkExtractor, attributes, Arrays.asList(descriptors)); } /** @@ -430,7 +427,7 @@ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, * @return the snippet that will document the links */ public static LinksSnippet relaxedLinks(LinkExtractor linkExtractor, - Map attributes, List descriptors) { + Map attributes, List descriptors) { return new LinksSnippet(linkExtractor, descriptors, attributes, true); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 464b7e2f1..9a552c92e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -26,6 +26,7 @@ * * @author Andreas Evers * @author Andy Wilkinson + * @author Marcel Overdijk */ public abstract class PayloadDocumentation { @@ -118,7 +119,7 @@ public static FieldDescriptor fieldWithPath(String path) { * @see #fieldWithPath(String) */ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) { - return new RequestFieldsSnippet(Arrays.asList(descriptors)); + return requestFields(Arrays.asList(descriptors)); } /** @@ -157,7 +158,7 @@ public static RequestFieldsSnippet requestFields(List descripto */ public static RequestFieldsSnippet relaxedRequestFields( FieldDescriptor... descriptors) { - return new RequestFieldsSnippet(Arrays.asList(descriptors), true); + return relaxedRequestFields(Arrays.asList(descriptors)); } /** @@ -199,7 +200,7 @@ public static RequestFieldsSnippet relaxedRequestFields( */ public static RequestFieldsSnippet requestFields(Map attributes, FieldDescriptor... descriptors) { - return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes); + return requestFields(attributes, Arrays.asList(descriptors)); } /** @@ -243,7 +244,7 @@ public static RequestFieldsSnippet requestFields(Map attributes, */ public static RequestFieldsSnippet relaxedRequestFields( Map attributes, FieldDescriptor... descriptors) { - return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedRequestFields(attributes, Arrays.asList(descriptors)); } /** @@ -285,7 +286,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @see #fieldWithPath(String) */ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) { - return new ResponseFieldsSnippet(Arrays.asList(descriptors)); + return responseFields(Arrays.asList(descriptors)); } /** @@ -308,7 +309,8 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor * @return the snippet that will document the fields * @see #fieldWithPath(String) */ - public static ResponseFieldsSnippet responseFields(List descriptors) { + public static ResponseFieldsSnippet responseFields( + List descriptors) { return new ResponseFieldsSnippet(descriptors); } @@ -326,7 +328,7 @@ public static ResponseFieldsSnippet responseFields(List descrip */ public static ResponseFieldsSnippet relaxedResponseFields( FieldDescriptor... descriptors) { - return new ResponseFieldsSnippet(Arrays.asList(descriptors), true); + return relaxedResponseFields(Arrays.asList(descriptors)); } /** @@ -369,7 +371,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( */ public static ResponseFieldsSnippet responseFields(Map attributes, FieldDescriptor... descriptors) { - return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes); + return responseFields(attributes, Arrays.asList(descriptors)); } /** @@ -413,10 +415,11 @@ public static ResponseFieldsSnippet responseFields(Map attribute */ public static ResponseFieldsSnippet relaxedResponseFields( Map attributes, FieldDescriptor... descriptors) { - return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedResponseFields(attributes, Arrays.asList(descriptors)); } - /** Returns a {@code Snippet} that will document the fields of the API operation's + /** + * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} * and the given {@code attributes} will be available during snippet generation. *

          @@ -447,8 +450,8 @@ public static List applyPathPrefix(String pathPrefix, for (FieldDescriptor descriptor : descriptors) { FieldDescriptor prefixedDescriptor = new FieldDescriptor( pathPrefix + descriptor.getPath()) - .description(descriptor.getDescription()) - .type(descriptor.getType()); + .description(descriptor.getDescription()) + .type(descriptor.getType()); if (descriptor.isIgnored()) { prefixedDescriptor.ignored(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java index c343cd7c7..043fdfe6f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java @@ -26,6 +26,7 @@ * Static factory methods for documenting aspects of a request sent to a RESTful API. * * @author Andy Wilkinson + * @author Marcel Overdijk */ public abstract class RequestDocumentation { @@ -74,7 +75,7 @@ public static RequestPartDescriptor partWithName(String name) { */ public static PathParametersSnippet pathParameters( ParameterDescriptor... descriptors) { - return new PathParametersSnippet(Arrays.asList(descriptors)); + return pathParameters(Arrays.asList(descriptors)); } /** @@ -112,7 +113,7 @@ public static PathParametersSnippet pathParameters( */ public static PathParametersSnippet relaxedPathParameters( ParameterDescriptor... descriptors) { - return new PathParametersSnippet(Arrays.asList(descriptors), true); + return relaxedPathParameters(Arrays.asList(descriptors)); } /** @@ -152,7 +153,7 @@ public static PathParametersSnippet relaxedPathParameters( */ public static PathParametersSnippet pathParameters(Map attributes, ParameterDescriptor... descriptors) { - return new PathParametersSnippet(Arrays.asList(descriptors), attributes); + return pathParameters(attributes, Arrays.asList(descriptors)); } /** @@ -194,7 +195,7 @@ public static PathParametersSnippet pathParameters(Map attribute */ public static PathParametersSnippet relaxedPathParameters( Map attributes, ParameterDescriptor... descriptors) { - return new PathParametersSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedPathParameters(attributes, Arrays.asList(descriptors)); } /** @@ -235,7 +236,7 @@ public static PathParametersSnippet relaxedPathParameters( */ public static RequestParametersSnippet requestParameters( ParameterDescriptor... descriptors) { - return new RequestParametersSnippet(Arrays.asList(descriptors)); + return requestParameters(Arrays.asList(descriptors)); } /** @@ -275,7 +276,7 @@ public static RequestParametersSnippet requestParameters( */ public static RequestParametersSnippet relaxedRequestParameters( ParameterDescriptor... descriptors) { - return new RequestParametersSnippet(Arrays.asList(descriptors), true); + return relaxedRequestParameters(Arrays.asList(descriptors)); } /** @@ -317,7 +318,7 @@ public static RequestParametersSnippet relaxedRequestParameters( */ public static RequestParametersSnippet requestParameters( Map attributes, ParameterDescriptor... descriptors) { - return new RequestParametersSnippet(Arrays.asList(descriptors), attributes); + return requestParameters(attributes, Arrays.asList(descriptors)); } /** @@ -361,7 +362,7 @@ public static RequestParametersSnippet requestParameters( */ public static RequestParametersSnippet relaxedRequestParameters( Map attributes, ParameterDescriptor... descriptors) { - return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedRequestParameters(attributes, Arrays.asList(descriptors)); } /** @@ -401,7 +402,7 @@ public static RequestParametersSnippet relaxedRequestParameters( * @see OperationRequest#getParts() */ public static RequestPartsSnippet requestParts(RequestPartDescriptor... descriptors) { - return new RequestPartsSnippet(Arrays.asList(descriptors)); + return requestParts(Arrays.asList(descriptors)); } /** @@ -421,7 +422,8 @@ public static RequestPartsSnippet requestParts(RequestPartDescriptor... descript * @return the snippet * @see OperationRequest#getParts() */ - public static RequestPartsSnippet requestParts(List descriptors) { + public static RequestPartsSnippet requestParts( + List descriptors) { return new RequestPartsSnippet(descriptors); } @@ -438,7 +440,7 @@ public static RequestPartsSnippet requestParts(List descr */ public static RequestPartsSnippet relaxedRequestParts( RequestPartDescriptor... descriptors) { - return new RequestPartsSnippet(Arrays.asList(descriptors), true); + return relaxedRequestParts(Arrays.asList(descriptors)); } /** @@ -478,7 +480,7 @@ public static RequestPartsSnippet relaxedRequestParts( */ public static RequestPartsSnippet requestParts(Map attributes, RequestPartDescriptor... descriptors) { - return new RequestPartsSnippet(Arrays.asList(descriptors), attributes); + return requestParts(attributes, Arrays.asList(descriptors)); } /** @@ -520,7 +522,7 @@ public static RequestPartsSnippet requestParts(Map attributes, */ public static RequestPartsSnippet relaxedRequestParts(Map attributes, RequestPartDescriptor... descriptors) { - return new RequestPartsSnippet(Arrays.asList(descriptors), attributes, true); + return relaxedRequestParts(attributes, Arrays.asList(descriptors)); } /** From 4715351cce088d1aa121fea7ae8986361a803e1d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 20 May 2016 16:12:12 +0200 Subject: [PATCH 126/898] Make it easier to provide a placeholder resolver that uses the context Closes gh-235 --- .../config/RestDocumentationConfigurer.java | 4 +- .../snippet/PlaceholderResolverFactory.java | 38 +++++++++++++++++++ ...tionContextPlaceholderResolverFactory.java | 37 ++++++++++++++++++ .../snippet/StandardWriterResolver.java | 37 +++++++++++++----- .../snippet/StandardWriterResolverTests.java | 7 ++-- .../restdocs/test/OperationBuilder.java | 4 +- 6 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 2b1216f4a..c6cb1ffc4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -23,7 +23,7 @@ import org.springframework.restdocs.RestDocumentationContext; import org.springframework.restdocs.mustache.Mustache; -import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; @@ -138,7 +138,7 @@ public void apply(Map configuration, SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration .get(SnippetConfiguration.class.getName()); resolverToUse = new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver(context), + new RestDocumentationContextPlaceholderResolverFactory(), snippetConfiguration.getEncoding(), snippetConfiguration.getTemplateFormat()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java new file mode 100644 index 000000000..6aec68d7b --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2016 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 org.springframework.restdocs.RestDocumentationContext; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * A factory for creating {@link PlaceholderResolver} instances. + * + * @author Andy Wilkinson + * @since 1.1 + */ +public interface PlaceholderResolverFactory { + + /** + * Creates a new {@link PlaceholderResolver} using the given {@code context}. + * + * @param context the context + * @return the placeholder resolver + */ + PlaceholderResolver create(RestDocumentationContext context); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java new file mode 100644 index 000000000..0ddb24562 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2016 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 org.springframework.restdocs.RestDocumentationContext; +import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; + +/** + * A {@link PlaceholderResolverFactory} that creates + * {@link RestDocumentationContextPlaceholderResolver} instances. + * + * @author Andy Wilkinson + * @since 1.1 + */ +public final class RestDocumentationContextPlaceholderResolverFactory + implements PlaceholderResolverFactory { + + @Override + public PlaceholderResolver create(RestDocumentationContext context) { + return new RestDocumentationContextPlaceholderResolver(context); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java index 479407ea0..2b4144931 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/StandardWriterResolver.java @@ -35,7 +35,7 @@ */ public final class StandardWriterResolver implements WriterResolver { - private final PlaceholderResolver placeholderResolver; + private final PlaceholderResolverFactory placeholderResolverFactory; private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper( "{", "}"); @@ -52,27 +52,29 @@ public final class StandardWriterResolver implements WriterResolver { * * @param placeholderResolver the placeholder resolver * @deprecated since 1.1.0 in favor of - * {@link #StandardWriterResolver(PropertyPlaceholderHelper.PlaceholderResolver, String, TemplateFormat)} + * {@link #StandardWriterResolver(PlaceholderResolverFactory, String, TemplateFormat)} */ @Deprecated public StandardWriterResolver(PlaceholderResolver placeholderResolver) { - this(placeholderResolver, "UTF-8", TemplateFormats.asciidoctor()); + this(new SingleInstancePlaceholderResolverFactory(placeholderResolver), "UTF-8", + TemplateFormats.asciidoctor()); } /** - * Creates a new {@code StandardWriterResolver} that will use the given - * {@code placeholderResolver} to resolve any placeholders in the + * Creates a new {@code StandardWriterResolver} that will use a + * {@link PlaceholderResolver} created from the given + * {@code placeholderResolverFactory} to resolve any placeholders in the * {@code operationName}. Writers will use the given {@code encoding} and, when * writing to a file, will use a filename appropriate for content generated from * templates in the given {@code templateFormat}. * - * @param placeholderResolver the placeholder resolver + * @param placeholderResolverFactory the placeholder resolver factory * @param encoding the encoding * @param templateFormat the snippet format */ - public StandardWriterResolver(PlaceholderResolver placeholderResolver, + public StandardWriterResolver(PlaceholderResolverFactory placeholderResolverFactory, String encoding, TemplateFormat templateFormat) { - this.placeholderResolver = placeholderResolver; + this.placeholderResolverFactory = placeholderResolverFactory; this.encoding = encoding; this.templateFormat = templateFormat; } @@ -82,7 +84,7 @@ public Writer resolve(String operationName, String snippetName, RestDocumentationContext context) throws IOException { File outputFile = resolveFile( this.propertyPlaceholderHelper.replacePlaceholders(operationName, - this.placeholderResolver), + this.placeholderResolverFactory.create(context)), snippetName + "." + this.templateFormat.getFileExtension(), context); if (outputFile != null) { @@ -127,4 +129,21 @@ private void createDirectoriesIfNecessary(File outputFile) { } } + private static final class SingleInstancePlaceholderResolverFactory + implements PlaceholderResolverFactory { + + private final PlaceholderResolver placeholderResolver; + + private SingleInstancePlaceholderResolverFactory( + PlaceholderResolver placeholderResolver) { + this.placeholderResolver = placeholderResolver; + } + + @Override + public PlaceholderResolver create(RestDocumentationContext context) { + return this.placeholderResolver; + } + + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index af5ccd149..a4d3460d6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -22,7 +22,6 @@ import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.restdocs.RestDocumentationContext; -import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; @@ -36,11 +35,11 @@ */ public class StandardWriterResolverTests { - private final PlaceholderResolver placeholderResolver = mock( - PlaceholderResolver.class); + private final PlaceholderResolverFactory placeholderResolverFactory = mock( + PlaceholderResolverFactory.class); private final StandardWriterResolver resolver = new StandardWriterResolver( - this.placeholderResolver, "UTF-8", asciidoctor()); + this.placeholderResolverFactory, "UTF-8", asciidoctor()); @Test public void absoluteInput() { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 4e292b2eb..3032a6f5d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -39,7 +39,7 @@ import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.StandardOperation; -import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolver; +import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; import org.springframework.restdocs.templates.StandardTemplateResourceResolver; @@ -107,7 +107,7 @@ public Operation build() { this.attributes.put(RestDocumentationContext.class.getName(), context); this.attributes.put(WriterResolver.class.getName(), new StandardWriterResolver( - new RestDocumentationContextPlaceholderResolver(context), "UTF-8", + new RestDocumentationContextPlaceholderResolverFactory(), "UTF-8", this.templateFormat)); return new StandardOperation(this.name, (this.requestBuilder == null From 6935f603f28e6e0d9dd8b1ec49b0b53dbfe8c1fc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 16:27:03 +0100 Subject: [PATCH 127/898] Polish ResourceBundleConstraintDescriptionResolver and related tests Closes gh-234 --- build.gradle | 5 +- spring-restdocs-core/build.gradle | 2 +- ...ceBundleConstraintDescriptionResolver.java | 31 +++ .../ConstraintDescriptionsTests.java | 182 +++--------------- 4 files changed, 65 insertions(+), 155 deletions(-) diff --git a/build.gradle b/build.gradle index 02b0977a5..38edf1e73 100644 --- a/build.gradle +++ b/build.gradle @@ -37,8 +37,9 @@ ext { springVersion = '4.2.5.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', - "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", - 'https://docs.jboss.org/hibernate/stable/beanvalidation/api/' + 'http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/', + 'https://docs.jboss.org/hibernate/stable/beanvalidation/api/', + 'https://docs.jboss.org/hibernate/stable/validator/api/' ] as String[] } diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 01ef4b9cc..1bee13f54 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -34,10 +34,10 @@ dependencies { optional 'commons-codec:commons-codec' optional 'javax.validation:validation-api' optional 'junit:junit' + optional 'org.hibernate:hibernate-validator' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' - testCompile 'org.hibernate:hibernate-validator' testCompile 'org.springframework:spring-test' testRuntime 'org.glassfish:javax.el:3.0.0' } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java index 759950a64..c55596c33 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/constraints/ResourceBundleConstraintDescriptionResolver.java @@ -34,6 +34,19 @@ import javax.validation.constraints.Pattern; import javax.validation.constraints.Size; +import org.hibernate.validator.constraints.CreditCardNumber; +import org.hibernate.validator.constraints.EAN; +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.LuhnCheck; +import org.hibernate.validator.constraints.Mod10Check; +import org.hibernate.validator.constraints.Mod11Check; +import org.hibernate.validator.constraints.NotBlank; +import org.hibernate.validator.constraints.NotEmpty; +import org.hibernate.validator.constraints.Range; +import org.hibernate.validator.constraints.SafeHtml; +import org.hibernate.validator.constraints.URL; + import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -62,6 +75,24 @@ *

        • {@link Size} * * + *

          + * Default descriptions are also provided for Hibernate Validator's constraints: + * + *

            + *
          • {@link CreditCardNumber} + *
          • {@link EAN} + *
          • {@link Email} + *
          • {@link Length} + *
          • {@link LuhnCheck} + *
          • {@link Mod10Check} + *
          • {@link Mod11Check} + *
          • {@link NotBlank} + *
          • {@link NotEmpty} + *
          • {@link Range} + *
          • {@link SafeHtml} + *
          • {@link URL} + *
          + * * @author Andy Wilkinson */ public class ResourceBundleConstraintDescriptionResolver diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java index 34e916c56..70c0d1abc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/constraints/ConstraintDescriptionsTests.java @@ -16,28 +16,17 @@ package org.springframework.restdocs.constraints; -import java.math.BigDecimal; -import java.util.Date; - -import javax.validation.constraints.AssertFalse; -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.DecimalMax; -import javax.validation.constraints.DecimalMin; -import javax.validation.constraints.Digits; -import javax.validation.constraints.Future; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Null; -import javax.validation.constraints.Past; -import javax.validation.constraints.Pattern; -import javax.validation.constraints.Size; +import java.util.Arrays; +import java.util.Collections; import org.junit.Test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; /** * Tests for {@link ConstraintDescriptions}. @@ -46,152 +35,41 @@ */ public class ConstraintDescriptionsTests { - private final ConstraintDescriptions constraintDescriptions = new ConstraintDescriptions( - Constrained.class); - - @Test - public void assertFalse() { - assertThat(this.constraintDescriptions.descriptionsForProperty("assertFalse"), - contains("Must be false")); - } + private final ConstraintResolver constraintResolver = mock(ConstraintResolver.class); - @Test - public void assertTrue() { - assertThat(this.constraintDescriptions.descriptionsForProperty("assertTrue"), - contains("Must be true")); - } + private final ConstraintDescriptionResolver constraintDescriptionResolver = mock( + ConstraintDescriptionResolver.class); - @Test - public void decimalMax() { - assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMax"), - contains("Must be at most 9.875")); - } - - @Test - public void decimalMin() { - assertThat(this.constraintDescriptions.descriptionsForProperty("decimalMin"), - contains("Must be at least 1.5")); - } - - @Test - public void digits() { - assertThat(this.constraintDescriptions.descriptionsForProperty("digits"), - contains("Must have at most 2 integral digits and 5 fractional digits")); - } - - @Test - public void future() { - assertThat(this.constraintDescriptions.descriptionsForProperty("future"), - contains("Must be in the future")); - } - - @Test - public void max() { - assertThat(this.constraintDescriptions.descriptionsForProperty("max"), - contains("Must be at most 10")); - } - - @Test - public void min() { - assertThat(this.constraintDescriptions.descriptionsForProperty("min"), - contains("Must be at least 5")); - } - - @Test - public void notNull() { - assertThat(this.constraintDescriptions.descriptionsForProperty("notNull"), - contains("Must not be null")); - } - - @Test - public void nul() { - assertThat(this.constraintDescriptions.descriptionsForProperty("nul"), - contains("Must be null")); - } - - @Test - public void past() { - assertThat(this.constraintDescriptions.descriptionsForProperty("past"), - contains("Must be in the past")); - } - - @Test - public void pattern() { - assertThat(this.constraintDescriptions.descriptionsForProperty("pattern"), - contains("Must match the regular expression `[A-Z][a-z]+`")); - } - - @Test - public void size() { - assertThat(this.constraintDescriptions.descriptionsForProperty("size"), - contains("Size must be between 0 and 10 inclusive")); - } - - @Test - public void sizeList() { - assertThat(this.constraintDescriptions.descriptionsForProperty("sizeList"), - contains("Size must be between 1 and 4 inclusive", - "Size must be between 8 and 10 inclusive")); - } + private final ConstraintDescriptions constraintDescriptions = new ConstraintDescriptions( + Constrained.class, this.constraintResolver, + this.constraintDescriptionResolver); @Test - public void unconstrained() { - assertThat(this.constraintDescriptions.descriptionsForProperty("unconstrained"), - hasSize(0)); + public void descriptionsForConstraints() { + Constraint constraint1 = new Constraint("constraint1", + Collections.emptyMap()); + Constraint constraint2 = new Constraint("constraint2", + Collections.emptyMap()); + given(this.constraintResolver.resolveForProperty("foo", Constrained.class)) + .willReturn(Arrays.asList(constraint1, constraint2)); + given(this.constraintDescriptionResolver.resolveDescription(constraint1)) + .willReturn("Bravo"); + given(this.constraintDescriptionResolver.resolveDescription(constraint2)) + .willReturn("Alpha"); + assertThat(this.constraintDescriptions.descriptionsForProperty("foo"), + contains("Alpha", "Bravo")); } @Test - public void nonExistentProperty() { - assertThat(this.constraintDescriptions.descriptionsForProperty("doesNotExist"), - hasSize(0)); + public void emptyListOfDescriptionsWhenThereAreNoConstraints() { + given(this.constraintResolver.resolveForProperty("foo", Constrained.class)) + .willReturn(Collections.emptyList()); + assertThat(this.constraintDescriptions.descriptionsForProperty("foo").size(), + is(equalTo(0))); } private static class Constrained { - @AssertFalse - private boolean assertFalse; - - @AssertTrue - private boolean assertTrue; - - @DecimalMax("9.875") - private BigDecimal decimalMax; - - @DecimalMin("1.5") - private BigDecimal decimalMin; - - @Digits(fraction = 5, integer = 2) - private BigDecimal digits; - - @Future - private Date future; - - @NotNull - private String notNull; - - @Max(10) - private int max; - - @Min(5) - private int min; - - @Null - private String nul; - - @Past - private Date past; - - @Pattern(regexp = "[A-Z][a-z]+") - private String pattern; - - @Size(min = 0, max = 10) - private String size; - - @Size.List({ @Size(min = 1, max = 4), @Size(min = 8, max = 10) }) - private String sizeList; - - @SuppressWarnings("unused") - private String unconstrained; } } From cb1f98c601ecedd0bcc20fb857dfbbceca3dd0b9 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 24 May 2016 16:39:57 +0100 Subject: [PATCH 128/898] Improve handling of parameters with null values Closes gh-199 --- .../restdocs/operation/Parameters.java | 4 + .../restdocs/operation/ParametersTests.java | 77 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java index 0235ec800..b696c2927 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.StringUtils; /** * The parameters received in a request. @@ -68,6 +69,9 @@ private static void doAppend(StringBuilder sb, String toAppend) { } private static String urlEncodeUTF8(String s) { + if (!StringUtils.hasLength(s)) { + return ""; + } try { return URLEncoder.encode(s, "UTF-8"); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java new file mode 100644 index 000000000..3c9228861 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/ParametersTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2014-2016 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.operation; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link Parameters}. + * + * @author Andy Wilkinson + */ +public class ParametersTests { + + private final Parameters parameters = new Parameters(); + + @Test + public void queryStringForNoParameters() { + assertThat(this.parameters.toQueryString(), is(equalTo(""))); + } + + @Test + public void queryStringForSingleParameter() { + this.parameters.add("a", "b"); + assertThat(this.parameters.toQueryString(), is(equalTo("a=b"))); + } + + @Test + public void queryStringForSingleParameterWithMultipleValues() { + this.parameters.add("a", "b"); + this.parameters.add("a", "c"); + assertThat(this.parameters.toQueryString(), is(equalTo("a=b&a=c"))); + } + + @Test + public void queryStringForMutipleParameters() { + this.parameters.add("a", "alpha"); + this.parameters.add("b", "bravo"); + assertThat(this.parameters.toQueryString(), is(equalTo("a=alpha&b=bravo"))); + } + + @Test + public void queryStringForParameterWithEmptyValue() { + this.parameters.add("a", ""); + assertThat(this.parameters.toQueryString(), is(equalTo("a="))); + } + + @Test + public void queryStringForParameterWithNullValue() { + this.parameters.add("a", null); + assertThat(this.parameters.toQueryString(), is(equalTo("a="))); + } + + @Test + public void queryStringForParameterThatRequiresEncoding() { + this.parameters.add("a", "alpha&bravo"); + assertThat(this.parameters.toQueryString(), is(equalTo("a=alpha%26bravo"))); + } + +} From 42353a0b5f6ef2c384640b51870a7adac0d53ece Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 10:46:39 +0100 Subject: [PATCH 129/898] Improve the API for generating additional snippets Previously, if alwaysDo was used and the user wanted to generate one or more additional snippets when calling perform a separate call to `snippets` was made prior to calling `perform`. This had two problems: - it required the result handler to be stateful (see gh-243) - it wasn't clear that the additional snippets would be produced when a subsequent call to perform was made This commit introduces a new API that allows the additional snippets to be specified within the MockMvc call. The old API has been deprecated and will be removed in 2.0. Closes gh-249 --- .../customizing-requests-and-responses.adoc | 8 +-- .../mockmvc/EveryTestPreprocessing.java | 15 +++--- .../restassured/EveryTestPreprocessing.java | 12 ++--- .../generate/RestDocumentationGenerator.java | 17 ++++++ .../RestDocumentationGeneratorTests.java | 31 +++++++++++ .../RestDocumentationResultHandler.java | 35 ++++++++++++ ...kMvcRestDocumentationIntegrationTests.java | 20 +++++++ .../restassured/RestDocumentationFilter.java | 53 ++++++++++++++++--- ...uredRestDocumentationIntegrationTests.java | 19 +++++++ 9 files changed, 186 insertions(+), 24 deletions(-) diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index 04a1b48e1..8f0ffaf0c 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -58,17 +58,17 @@ Then, in each test, any configuration specific to that test can be performed. Fo ---- include::{examples-dir}/com/example/mockmvc/EveryTestPreprocessing.java[tags=use] ---- -<1> Document the links specific to the resource that is being tested -<2> The request and response will be preprocessed due to the use of `alwaysDo` above. +<1> The request and response will be preprocessed due to the use of `alwaysDo` above. +<2> Document the links specific to the resource that is being tested [source,java,indent=0,role="secondary"] .REST Assured ---- include::{examples-dir}/com/example/restassured/EveryTestPreprocessing.java[tags=use] ---- -<1> Document the links specific to the resource that is being tested -<2> The request and response will be preprocessed due to the configuration of the +<1> The request and response will be preprocessed due to the configuration of the `RequestSpecification` in the `setUp` method. +<2> Document the links specific to the resource that is being tested Various built in preprocessors, including those illustrated above, are available via the static methods on `Preprocessors`. See <> for further details. diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index 18799410e..31d883549 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -46,16 +46,16 @@ public class EveryTestPreprocessing { // tag::setup[] private MockMvc mockMvc; - private RestDocumentationResultHandler document; + private RestDocumentationResultHandler documentationHandler; @Before public void setup() { - this.document = document("{method-name}", // <1> + this.documentationHandler = document("{method-name}", // <1> preprocessRequest(removeHeaders("Foo")), preprocessResponse(prettyPrint())); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(this.document) // <2> + .alwaysDo(this.documentationHandler) // <2> .build(); } @@ -63,10 +63,11 @@ public void setup() { public void use() throws Exception { // tag::use[] - this.document.snippets( // <1> - links(linkWithRel("self").description("Canonical self link"))); - this.mockMvc.perform(get("/")) // <2> - .andExpect(status().isOk()); + this.mockMvc.perform(get("/")) // <1> + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( // <2> + links(linkWithRel("self").description("Canonical self link")) + )); // end::use[] } diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index e143bcc26..43ef84e79 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -44,16 +44,16 @@ public class EveryTestPreprocessing { // tag::setup[] private RequestSpecification spec; - private RestDocumentationFilter document; + private RestDocumentationFilter documentationFilter; @Before public void setup() { - this.document = document("{method-name}", + this.documentationFilter = document("{method-name}", preprocessRequest(removeHeaders("Foo")), preprocessResponse(prettyPrint())); // <1> this.spec = new RequestSpecBuilder() .addFilter(documentationConfiguration(this.restDocumentation)) - .addFilter(this.document)// <2> + .addFilter(this.documentationFilter)// <2> .build(); } @@ -61,9 +61,9 @@ public void setup() { public void use() throws Exception { // tag::use[] - this.document.snippets( // <1> - links(linkWithRel("self").description("Canonical self link"))); - RestAssured.given(this.spec) // <2> + RestAssured.given(this.spec) // <1> + .filter(this.documentationFilter.document( // <2> + links(linkWithRel("self").description("Canonical self link")))) .when().get("/") .then().assertThat().statusCode(is(200)); // end::use[] diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java index 52f9cb094..8a2b0402c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java @@ -41,6 +41,7 @@ * @param the request type that can be handled * @param the response type that can be handled * @author Andy Wilkinson + * @since 1.1 */ public final class RestDocumentationGenerator { @@ -205,11 +206,27 @@ public void handle(REQ request, RESP response, Map configuration * called. * * @param snippets the snippets to add + * @deprecated since 1.1 in favor of {@link #withSnippets(Snippet...)} */ + @Deprecated public void addSnippets(Snippet... snippets) { this.additionalSnippets.addAll(Arrays.asList(snippets)); } + /** + * Creates a new {@link RestDocumentationGenerator} with the same configuration as + * this one other than its snippets. The new generator will use the given + * {@code snippets}. + * + * @param snippets the snippets + * @return the new generator + */ + public RestDocumentationGenerator withSnippets(Snippet... snippets) { + return new RestDocumentationGenerator<>(this.identifier, this.requestConverter, + this.responseConverter, this.requestPreprocessor, + this.responsePreprocessor, snippets); + } + @SuppressWarnings("unchecked") private List getSnippets(Map configuration) { List combinedSnippets = new ArrayList<>(this.snippets); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index fd9b032d3..133974a75 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -35,6 +35,8 @@ import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.RequestConverter; import org.springframework.restdocs.operation.ResponseConverter; +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.restdocs.snippet.Snippet; import static org.hamcrest.CoreMatchers.equalTo; @@ -43,6 +45,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link RestDocumentationGenerator}. @@ -105,6 +108,7 @@ public void defaultSnippetsAreCalled() throws IOException { } @Test + @Deprecated public void additionalSnippetsAreCalled() throws IOException { given(this.requestConverter.convert(this.request)) .willReturn(this.operationRequest); @@ -123,6 +127,33 @@ public void additionalSnippetsAreCalled() throws IOException { verifySnippetInvocation(additionalSnippet2, configuration); } + @Test + public void newGeneratorOnlyCallsItsSnippets() throws IOException { + OperationRequestPreprocessor requestPreprocessor = mock( + OperationRequestPreprocessor.class); + OperationResponsePreprocessor responsePreprocessor = mock( + OperationResponsePreprocessor.class); + given(this.requestConverter.convert(this.request)) + .willReturn(this.operationRequest); + given(this.responseConverter.convert(this.response)) + .willReturn(this.operationResponse); + given(requestPreprocessor.preprocess(this.operationRequest)) + .willReturn(this.operationRequest); + given(responsePreprocessor.preprocess(this.operationResponse)) + .willReturn(this.operationResponse); + Snippet additionalSnippet1 = mock(Snippet.class); + Snippet additionalSnippet2 = mock(Snippet.class); + RestDocumentationGenerator generator = new RestDocumentationGenerator<>( + "id", this.requestConverter, this.responseConverter, requestPreprocessor, + responsePreprocessor, this.snippet); + HashMap configuration = new HashMap<>(); + generator.withSnippets(additionalSnippet1, additionalSnippet2) + .handle(this.request, this.response, configuration); + verifyNoMoreInteractions(this.snippet); + verifySnippetInvocation(additionalSnippet1, configuration); + verifySnippetInvocation(additionalSnippet2, configuration); + } + private void verifySnippetInvocation(Snippet snippet, Map attributes) throws IOException { verifySnippetInvocation(snippet, attributes, 1); diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index ac43bdd68..5a85fc5c2 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.mockmvc; +import java.util.HashMap; import java.util.Map; import org.springframework.mock.web.MockHttpServletRequest; @@ -59,10 +60,44 @@ public void handle(MvcResult result) throws Exception { * * @param snippets the snippets to add * @return this {@code RestDocumentationResultHandler} + * @deprecated since 1.1 in favor of {@link #document(Snippet...)} */ + @Deprecated public RestDocumentationResultHandler snippets(Snippet... snippets) { this.delegate.addSnippets(snippets); return this; } + /** + * Creates a new {@link RestDocumentationResultHandler} that will produce + * documentation using the given {@code snippets}. + * + * @param snippets the snippets + * @return the new result handler + */ + public RestDocumentationResultHandler document(Snippet... snippets) { + return new RestDocumentationResultHandler(this.delegate.withSnippets(snippets)) { + + @Override + public void handle(MvcResult result) throws Exception { + @SuppressWarnings("unchecked") + Map configuration = new HashMap<>( + (Map) result.getRequest() + .getAttribute(ATTRIBUTE_NAME_CONFIGURATION)); + configuration.remove( + RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + getDelegate().handle(result.getRequest(), result.getResponse(), + configuration); + } + }; + } + + /** + * Returns the {@link RestDocumentationGenerator} that is used as a delegate. + * + * @return the delegate + */ + protected final RestDocumentationGenerator getDelegate() { + return this.delegate; + } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 5068d34fc..5d70cfb57 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -56,6 +56,8 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.springframework.restdocs.cli.CliDocumentation.curlRequest; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -329,6 +331,24 @@ public void multiStep() throws Exception { "http-response.adoc", "curl-request.adoc"); } + @Test + public void alwaysDoWithAdditionalSnippets() throws Exception { + RestDocumentationResultHandler documentation = document("{method-name}-{step}"); + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(documentation).build(); + + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(documentation.document( + responseHeaders(headerWithName("a").description("one")))); + + assertExpectedSnippetFilesExist( + new File( + "build/generated-snippets/always-do-with-additional-snippets-1/"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "response-headers.adoc"); + } + @Test public void preprocessedRequest() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 3ee03d3fb..0d37351e8 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -35,7 +35,7 @@ * * @author Andy Wilkinson */ -public final class RestDocumentationFilter implements Filter { +public class RestDocumentationFilter implements Filter { static final String CONTEXT_KEY_CONFIGURATION = "org.springframework.restdocs.configuration"; @@ -48,10 +48,27 @@ public final class RestDocumentationFilter implements Filter { } @Override - public Response filter(FilterableRequestSpecification requestSpec, + public final Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext context) { Response response = context.next(requestSpec, responseSpec); + Map configuration = getConfiguration(requestSpec, context); + + this.delegate.handle(requestSpec, response, configuration); + + return response; + } + + /** + * Returns the configuration that should be used when calling the delgate. The + * configuration is derived from the given {@code requestSpec} and {@code context}. + * + * @param requestSpec the request specification + * @param context the filter context + * @return the configuration + */ + protected Map getConfiguration( + FilterableRequestSpecification requestSpec, FilterContext context) { Map configuration = new HashMap<>( context.>getValue(CONTEXT_KEY_CONFIGURATION)); configuration.put(RestDocumentationContext.class.getName(), @@ -59,10 +76,7 @@ public Response filter(FilterableRequestSpecification requestSpec, RestDocumentationContext.class.getName())); configuration.put(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, requestSpec.getUserDefinedPath()); - - this.delegate.handle(requestSpec, response, configuration); - - return response; + return configuration; } /** @@ -71,10 +85,35 @@ public Response filter(FilterableRequestSpecification requestSpec, * * @param snippets the snippets to add * @return this {@code RestDocumentationFilter} + * @deprecated since 1.1 in favor of {@link #document(Snippet...)} */ - public RestDocumentationFilter snippets(Snippet... snippets) { + @Deprecated + public final RestDocumentationFilter snippets(Snippet... snippets) { this.delegate.addSnippets(snippets); return this; } + /** + * Creates a new {@link RestDocumentationFilter} that will produce documentation using + * the given {@code snippets}. + * + * @param snippets the snippets + * @return the new result handler + */ + public final RestDocumentationFilter document(Snippet... snippets) { + return new RestDocumentationFilter(this.delegate.withSnippets(snippets)) { + + @Override + protected Map getConfiguration( + FilterableRequestSpecification requestSpec, FilterContext context) { + Map configuration = super.getConfiguration(requestSpec, + context); + configuration.remove( + RestDocumentationGenerator.ATTRIBUTE_NAME_DEFAULT_SNIPPETS); + return configuration; + } + + }; + } + } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 658793ee7..27c055ae0 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -52,6 +52,8 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; import static org.springframework.restdocs.operation.preprocess.Preprocessors.maskLinks; @@ -240,6 +242,23 @@ public void multiStep() throws Exception { "http-response.adoc", "curl-request.adoc"); } + @Test + public void additionalSnippets() throws Exception { + RestDocumentationFilter documentation = document("{method-name}-{step}"); + RequestSpecification spec = new RequestSpecBuilder().setPort(this.port) + .addFilter(documentationConfiguration(this.restDocumentation)) + .addFilter(documentation).build(); + given(spec) + .filter(documentation + .document(responseHeaders(headerWithName("a").description("one"), + headerWithName("Foo").description("two")))) + .get("/").then().statusCode(200); + assertExpectedSnippetFilesExist( + new File("build/generated-snippets/additional-snippets-1/"), + "http-request.adoc", "http-response.adoc", "curl-request.adoc", + "response-headers.adoc"); + } + @Test public void preprocessedRequest() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); From 0446c99a101a423c99003c8fa5a6a551f374865c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 11:26:02 +0100 Subject: [PATCH 130/898] Polishing - Correct copyright years - Add `@since` to javadoc of new public classes - Make new classes final where appropriate --- config/eclipse/org.eclipse.jdt.ui.prefs | 2 +- docs/src/test/java/com/example/SnippetReuse.java | 2 +- .../test/java/com/example/mockmvc/MockMvcSnippetReuse.java | 6 +++--- .../com/example/restassured/RestAssuredSnippetReuse.java | 6 +++--- .../springframework/restdocs/ManualRestDocumentation.java | 2 +- .../restdocs/RestDocumentationContextProvider.java | 2 +- .../org/springframework/restdocs/cli/CliDocumentation.java | 3 ++- .../springframework/restdocs/cli/CliOperationRequest.java | 2 +- .../springframework/restdocs/cli/CurlRequestSnippet.java | 1 + .../springframework/restdocs/cli/HttpieRequestSnippet.java | 1 + .../springframework/restdocs/config/AbstractConfigurer.java | 1 + .../restdocs/config/AbstractNestedConfigurer.java | 1 + .../springframework/restdocs/config/NestedConfigurer.java | 1 + .../restdocs/config/RestDocumentationConfigurer.java | 1 + .../restdocs/config/SnippetConfiguration.java | 1 + .../springframework/restdocs/config/SnippetConfigurer.java | 1 + .../generate/RestDocumentationGenerationException.java | 3 ++- .../restdocs/generate/RestDocumentationGenerator.java | 2 +- .../org/springframework/restdocs/generate/package-info.java | 4 ++-- .../restdocs/operation/ConversionException.java | 3 ++- .../restdocs/operation/RequestConverter.java | 3 ++- .../restdocs/operation/ResponseConverter.java | 3 ++- .../operation/preprocess/OperationPreprocessorAdapter.java | 2 +- .../ParametersModifyingOperationPreprocessor.java | 2 +- .../restdocs/request/RequestPartDescriptor.java | 1 + .../restdocs/request/RequestPartsSnippet.java | 1 + .../restdocs/snippet/PlaceholderResolverFactory.java | 2 +- .../RestDocumentationContextPlaceholderResolverFactory.java | 2 +- .../springframework/restdocs/templates/TemplateFormat.java | 1 + .../springframework/restdocs/templates/TemplateFormats.java | 1 + .../mustache/AsciidoctorTableCellContentLambda.java | 1 + .../org/springframework/restdocs/AbstractSnippetTests.java | 2 +- .../restdocs/RestDocumentationGeneratorTests.java | 2 +- .../restdocs/cli/QueryStringParserTests.java | 2 +- .../restdocs/headers/RequestHeadersSnippetFailureTests.java | 2 +- .../headers/ResponseHeadersSnippetFailureTests.java | 2 +- .../restdocs/hypermedia/LinksSnippetFailureTests.java | 2 +- .../ParametersModifyingOperationPreprocessorTests.java | 2 +- .../payload/AsciidoctorRequestFieldsSnippetTests.java | 2 +- .../restdocs/payload/RequestFieldsSnippetFailureTests.java | 2 +- .../restdocs/payload/ResponseFieldsSnippetFailureTests.java | 2 +- .../restdocs/request/PathParametersSnippetFailureTests.java | 2 +- .../request/RequestParametersSnippetFailureTests.java | 2 +- .../restdocs/request/RequestPartsSnippetFailureTests.java | 2 +- .../restdocs/snippet/TemplatedSnippetTests.java | 2 +- .../restdocs/mockmvc/MockMvcRequestConverter.java | 1 - .../mockmvc/MockMvcRestDocumentationConfigurer.java | 5 +++-- .../restdocs/mockmvc/MockMvcSnippetConfigurer.java | 3 ++- .../restdocs/restassured/RestAssuredRestDocumentation.java | 1 + .../restassured/RestAssuredRestDocumentationConfigurer.java | 1 + .../restdocs/restassured/RestAssuredSnippetConfigurer.java | 1 + .../restdocs/restassured/RestDocumentationFilter.java | 1 + .../operation/preprocess/RestAssuredPreprocessors.java | 3 ++- .../preprocess/UriModifyingOperationPreprocessor.java | 3 ++- .../restassured/RestAssuredRequestConverterTests.java | 2 +- .../RestAssuredRestDocumentationConfigurerTests.java | 2 +- .../preprocess/UriModifyingOperationPreprocessorTests.java | 2 +- 57 files changed, 71 insertions(+), 46 deletions(-) diff --git a/config/eclipse/org.eclipse.jdt.ui.prefs b/config/eclipse/org.eclipse.jdt.ui.prefs index 429b30236..d9bb135cd 100644 --- a/config/eclipse/org.eclipse.jdt.ui.prefs +++ b/config/eclipse/org.eclipse.jdt.ui.prefs @@ -70,7 +70,7 @@ org.eclipse.jdt.ui.keywordthis=false org.eclipse.jdt.ui.ondemandthreshold=9999 org.eclipse.jdt.ui.overrideannotation=true org.eclipse.jdt.ui.staticondemandthreshold=9999 -org.eclipse.jdt.ui.text.custom_code_templates= +org.eclipse.jdt.ui.text.custom_code_templates= sp_cleanup.add_default_serial_version_id=true sp_cleanup.add_generated_serial_version_id=false sp_cleanup.add_missing_annotations=true diff --git a/docs/src/test/java/com/example/SnippetReuse.java b/docs/src/test/java/com/example/SnippetReuse.java index 6ab40ddb6..37ecc1acf 100644 --- a/docs/src/test/java/com/example/SnippetReuse.java +++ b/docs/src/test/java/com/example/SnippetReuse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java index 34dc3ae1b..f76eea97e 100644 --- a/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java +++ b/docs/src/test/java/com/example/mockmvc/MockMvcSnippetReuse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -27,9 +27,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class MockMvcSnippetReuse extends SnippetReuse { - + private MockMvc mockMvc; - + public void documentation() throws Exception { // tag::use[] this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) diff --git a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java index c792a7fbc..770ed4827 100644 --- a/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java +++ b/docs/src/test/java/com/example/restassured/RestAssuredSnippetReuse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -26,9 +26,9 @@ import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; public class RestAssuredSnippetReuse extends SnippetReuse { - + private RequestSpecification spec; - + public void documentation() throws Exception { // tag::use[] RestAssured.given(this.spec) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java index ff12d5910..890d9a4cd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java index 5dbe7a78c..e2a94aa59 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentationContextProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java index 9b6b7675f..b22a2b04b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -27,6 +27,7 @@ * @author Andy Wilkinson * @author Paul-Christian Volkmer * @author Raman Gupta + * @since 1.1.0 */ public abstract class CliDocumentation { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 99ce6aeba..97d64c256 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 5112b6dc5..f077c115a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -39,6 +39,7 @@ * @author Paul-Christian Volkmer * @see CliDocumentation#curlRequest() * @see CliDocumentation#curlRequest(Map) + * @since 1.1.0 */ public class CurlRequestSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 0d132e417..981ea5497 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -40,6 +40,7 @@ * @author Andy Wilkinson * @see CliDocumentation#httpieRequest() * @see CliDocumentation#httpieRequest(Map) + * @since 1.1.0 */ public class HttpieRequestSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java index 7a8a9e03e..fd46b3768 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractConfigurer.java @@ -25,6 +25,7 @@ * configuration implementation. * * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class AbstractConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java index 30e23a321..0af74d652 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/AbstractNestedConfigurer.java @@ -21,6 +21,7 @@ * * @param The type of the configurer's parent * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class AbstractNestedConfigurer extends AbstractConfigurer implements NestedConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java index 1ba13b80f..8d628b684 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/NestedConfigurer.java @@ -21,6 +21,7 @@ * * @param The parent's type * @author Andy Wilkinson + * @since 1.1.0 */ interface NestedConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index c6cb1ffc4..59444e655 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -39,6 +39,7 @@ * @param The concrete type of this configurer, to be returned from methods that * support chaining * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class RestDocumentationConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java index a1ced2167..caee04ca1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfiguration.java @@ -22,6 +22,7 @@ * An encapsulation of the configuration for documentation snippets. * * @author Andy Wilkinson + * @since 1.1.0 */ class SnippetConfiguration { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index d6475aadc..0da0b48c9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -35,6 +35,7 @@ * @param The type of the configurer's parent * @param The concrete type of the configurer to be returned from chained methods * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class SnippetConfigurer extends AbstractNestedConfigurer { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java index e1e76d8c2..2db29ea00 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -21,6 +21,7 @@ * generation. * * @author Andy Wilkinson + * @since 1.1.0 */ public class RestDocumentationGenerationException extends RuntimeException { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java index 8a2b0402c..c169b9d8a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/RestDocumentationGenerator.java @@ -41,7 +41,7 @@ * @param the request type that can be handled * @param the response type that can be handled * @author Andy Wilkinson - * @since 1.1 + * @since 1.1.0 */ public final class RestDocumentationGenerator { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java index 073c2dd8f..28a85f0d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/generate/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -15,6 +15,6 @@ */ /** - * Classes that drive the generation of the documentaiton snippets. + * Classes that drive the generation of the documentation snippets. */ package org.springframework.restdocs.generate; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java index 397f79d9e..8bf4643cf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -24,6 +24,7 @@ * @author Andy Wilkinson * @see RequestConverter#convert(Object) * @see ResponseConverter#convert(Object) + * @since 1.1.0 */ public class ConversionException extends RuntimeException { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java index 5c2377d5a..f7561ae58 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -22,6 +22,7 @@ * * @param The implementation-specific request type * @author Andy Wilkinson + * @since 1.1.0 */ public interface RequestConverter { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java index 48a76be28..d294e68d5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ResponseConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -22,6 +22,7 @@ * * @param The implementation-specific response type * @author Andy Wilkinson + * @since 1.1.0 */ public interface ResponseConverter { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java index 544de0f45..bf2aadfb7 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/OperationPreprocessorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java index 5006c73e3..71759dad9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java index 789649607..7cdb72bfa 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java @@ -23,6 +23,7 @@ * * @author Andy Wilkinson * @see RequestDocumentation#partWithName + * @since 1.1.0 */ public class RequestPartDescriptor extends IgnorableDescriptor { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java index c55bca4e2..1bf6cc35b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java @@ -42,6 +42,7 @@ * @see RequestDocumentation#requestParts(Map, RequestPartDescriptor...) * @see RequestDocumentation#relaxedRequestParts(RequestPartDescriptor...) * @see RequestDocumentation#relaxedRequestParts(Map, RequestPartDescriptor...) + * @since 1.1.0 */ public class RequestPartsSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java index 6aec68d7b..5683346b8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/PlaceholderResolverFactory.java @@ -23,7 +23,7 @@ * A factory for creating {@link PlaceholderResolver} instances. * * @author Andy Wilkinson - * @since 1.1 + * @since 1.1.0 */ public interface PlaceholderResolverFactory { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java index 0ddb24562..c364b14d6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverFactory.java @@ -24,7 +24,7 @@ * {@link RestDocumentationContextPlaceholderResolver} instances. * * @author Andy Wilkinson - * @since 1.1 + * @since 1.1.0 */ public final class RestDocumentationContextPlaceholderResolverFactory implements PlaceholderResolverFactory { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java index 972103607..fa75b5f5b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormat.java @@ -21,6 +21,7 @@ * as Asciidoctor or Markdown. * * @author Andy Wilkinson + * @since 1.1.0 */ public interface TemplateFormat { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java index 537f01ed5..1b76d0c0d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/TemplateFormats.java @@ -20,6 +20,7 @@ * An enumeration of the built-in formats for which templates are provuded. * * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class TemplateFormats { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java index 7881c6e08..a218da0e2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/templates/mustache/AsciidoctorTableCellContentLambda.java @@ -27,6 +27,7 @@ * formatting. * * @author Andy Wilkinson + * @since 1.1.0 */ public final class AsciidoctorTableCellContentLambda implements Lambda { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index 8969e0bcb..0adc3d0b5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java index 133974a75..f10712153 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/RestDocumentationGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java index 6a953e16a..3f30bd89e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java index 47b2024fe..33ccabb27 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java index cee2ebc22..997a3bc8a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index 217b753e5..b8f994703 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java index 4d8035f39..ebcef8ce1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/ParametersModifyingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index 070389ce2..c11243c5c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index 8b2760a9a..eeab98cfa 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index 34770b618..98a31d45a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 0abd322bc..99ab6a0ba 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java index 44d3165fe..79cea42b5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java index 93de349a5..b349f0f03 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java index 4213da4e5..f64034c67 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index ba96164ec..e933a0e0a 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -50,7 +50,6 @@ * {@link MockHttpServletRequest}. * * @author Andy Wilkinson - * */ class MockMvcRequestConverter implements RequestConverter { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java index a1a023c46..202d393f9 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -33,8 +33,9 @@ * A MockMvc-specific {@link RestDocumentationConfigurer}. * * @author Andy Wilkinson + * @since 1.1.0 */ -public class MockMvcRestDocumentationConfigurer extends +public final class MockMvcRestDocumentationConfigurer extends RestDocumentationConfigurer implements MockMvcConfigurer { diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java index a46302c01..a439bddc4 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcSnippetConfigurer.java @@ -26,8 +26,9 @@ * A configurer that can be used to configure the generated documentation snippets. * * @author Andy Wilkinson + * @since 1.1.0 */ -public class MockMvcSnippetConfigurer extends +public final class MockMvcSnippetConfigurer extends SnippetConfigurer implements MockMvcConfigurer { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java index 92aa38641..2cb6b4fa6 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentation.java @@ -26,6 +26,7 @@ * Static factory methods for documenting RESTful APIs using REST Assured. * * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class RestAssuredRestDocumentation { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java index 8ede6f745..6e64aa6fe 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurer.java @@ -33,6 +33,7 @@ * A REST Assured-specific {@link RestDocumentationConfigurer}. * * @author Andy Wilkinson + * @since 1.1.0 */ public final class RestAssuredRestDocumentationConfigurer extends RestDocumentationConfigurer diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java index 63ee75da5..3e1f5dee0 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredSnippetConfigurer.java @@ -29,6 +29,7 @@ * using REST Assured. * * @author Andy Wilkinson + * @since 1.1.0 */ public final class RestAssuredSnippetConfigurer extends SnippetConfigurer diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java index 0d37351e8..97387058b 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestDocumentationFilter.java @@ -34,6 +34,7 @@ * A REST Assured {@link Filter} for documenting RESTful APIs. * * @author Andy Wilkinson + * @since 1.1.0 */ public class RestDocumentationFilter implements Filter { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java index 5e5c1f9f9..b124544c1 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/RestAssuredPreprocessors.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -28,6 +28,7 @@ * {@link OperationResponse response} before it is documented. * * @author Andy Wilkinson + * @since 1.1.0 */ public abstract class RestAssuredPreprocessors { diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index 9063ac948..3c7b109d0 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. @@ -53,6 +53,7 @@ * * * @author Andy Wilkinson + * @since 1.1.0 */ public final class UriModifyingOperationPreprocessor implements OperationPreprocessor { diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 771fb60f5..389a878aa 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index 5da692491..d60b85ff3 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java index 60bda2db8..8ce532886 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2014-2016 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. From 251d93ddc8ffd29eb323dbf8bc0120787f9c7876 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 11:34:57 +0100 Subject: [PATCH 131/898] Correct ordering of Javadoc tags --- .../org/springframework/restdocs/cli/CurlRequestSnippet.java | 2 +- .../org/springframework/restdocs/cli/HttpieRequestSnippet.java | 2 +- .../springframework/restdocs/operation/ConversionException.java | 2 +- .../springframework/restdocs/request/RequestPartDescriptor.java | 2 +- .../springframework/restdocs/request/RequestPartsSnippet.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index f077c115a..73eeb72a3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -37,9 +37,9 @@ * * @author Andy Wilkinson * @author Paul-Christian Volkmer + * @since 1.1.0 * @see CliDocumentation#curlRequest() * @see CliDocumentation#curlRequest(Map) - * @since 1.1.0 */ public class CurlRequestSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 981ea5497..57673b9c3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -38,9 +38,9 @@ * * @author Raman Gupta * @author Andy Wilkinson + * @since 1.1.0 * @see CliDocumentation#httpieRequest() * @see CliDocumentation#httpieRequest(Map) - * @since 1.1.0 */ public class HttpieRequestSnippet extends TemplatedSnippet { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java index 8bf4643cf..96b6c5e59 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/ConversionException.java @@ -22,9 +22,9 @@ * during conversion. * * @author Andy Wilkinson + * @since 1.1.0 * @see RequestConverter#convert(Object) * @see ResponseConverter#convert(Object) - * @since 1.1.0 */ public class ConversionException extends RuntimeException { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java index 7cdb72bfa..5ca4e68bf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java @@ -22,8 +22,8 @@ * A descriptor of a request part. * * @author Andy Wilkinson - * @see RequestDocumentation#partWithName * @since 1.1.0 + * @see RequestDocumentation#partWithName */ public class RequestPartDescriptor extends IgnorableDescriptor { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java index 1bf6cc35b..7dc97e5a5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java @@ -38,11 +38,11 @@ * A {@link Snippet} that documents the request parts supported by a RESTful resource. * * @author Andy Wilkinson + * @since 1.1.0 * @see RequestDocumentation#requestParts(RequestPartDescriptor...) * @see RequestDocumentation#requestParts(Map, RequestPartDescriptor...) * @see RequestDocumentation#relaxedRequestParts(RequestPartDescriptor...) * @see RequestDocumentation#relaxedRequestParts(Map, RequestPartDescriptor...) - * @since 1.1.0 */ public class RequestPartsSnippet extends TemplatedSnippet { From 26ff2ec8a51421e15acf032bd7f19968ca1697a7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 12:02:53 +0100 Subject: [PATCH 132/898] Update HATEOAS sample to use new API for generating extra snippets See gh-249 --- .../com/example/notes/ApiDocumentation.java | 300 +++++++++--------- 1 file changed, 149 insertions(+), 151 deletions(-) diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 90abbdc62..b3cbb58f5 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -34,7 +34,6 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.key; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -73,7 +72,7 @@ public class ApiDocumentation { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); - private RestDocumentationResultHandler document; + private RestDocumentationResultHandler documentationHandler; @Autowired private NoteRepository noteRepository; @@ -91,77 +90,73 @@ public class ApiDocumentation { @Before public void setUp() { - this.document = document("{method-name}", - preprocessRequest(prettyPrint()), - preprocessResponse(prettyPrint())); + this.documentationHandler = document("{method-name}", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint())); this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) - .apply(documentationConfiguration(this.restDocumentation)) - .alwaysDo(this.document) - .build(); + .apply(documentationConfiguration(this.restDocumentation)) + .alwaysDo(this.documentationHandler) + .build(); } @Test public void headersExample() throws Exception { - this.document.snippets(responseHeaders( - headerWithName("Content-Type").description("The Content-Type of the payload, e.g. `application/hal+json`"))); - - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()); + this.mockMvc + .perform(get("/")) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( + responseHeaders( + headerWithName("Content-Type").description("The Content-Type of the payload, e.g. `application/hal+json`")))); } @Test public void errorExample() throws Exception { - this.document.snippets(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"))); - this.mockMvc .perform(get("/error") - .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) - .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, - "/notes") - .requestAttr(RequestDispatcher.ERROR_MESSAGE, - "The tag 'http://localhost:8080/tags/123' does not exist")) - .andDo(print()).andExpect(status().isBadRequest()) + .requestAttr(RequestDispatcher.ERROR_STATUS_CODE, 400) + .requestAttr(RequestDispatcher.ERROR_REQUEST_URI, "/notes") + .requestAttr(RequestDispatcher.ERROR_MESSAGE, "The tag 'http://localhost:8080/tags/123' does not exist")) + .andExpect(status().isBadRequest()) .andExpect(jsonPath("error", is("Bad Request"))) .andExpect(jsonPath("timestamp", is(notNullValue()))) .andExpect(jsonPath("status", is(400))) - .andExpect(jsonPath("path", is(notNullValue()))); + .andExpect(jsonPath("path", is(notNullValue()))) + .andDo(this.documentationHandler.document( + 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")))); } @Test public void indexExample() throws Exception { - this.document.snippets( + this.mockMvc.perform(get("/")) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( links( - linkWithRel("notes").description("The <>"), - linkWithRel("tags").description("The <>")), + linkWithRel("notes").description("The <>"), + linkWithRel("tags").description("The <>")), responseFields( - fieldWithPath("_links").description("<> to other resources"))); - - this.mockMvc.perform(get("/")) - .andExpect(status().isOk()); + fieldWithPath("_links").description("<> to other resources")))); } @Test public void notesListExample() throws Exception { this.noteRepository.deleteAll(); - createNote("REST maturity model", - "http://martinfowler.com/articles/richardsonMaturityModel.html"); - createNote("Hypertext Application Language (HAL)", - "http://stateless.co/hal_specification.html"); + createNote("REST maturity model", "http://martinfowler.com/articles/richardsonMaturityModel.html"); + createNote("Hypertext Application Language (HAL)", "http://stateless.co/hal_specification.html"); createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); - this.document.snippets( + this.mockMvc + .perform(get("/notes")) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( responseFields( - fieldWithPath("_embedded.notes").description("An array of <>"))); - - this.mockMvc.perform(get("/notes")) - .andExpect(status().isOk()); + fieldWithPath("_embedded.notes").description("An array of <>")))); } @Test @@ -170,11 +165,11 @@ public void notesCreateExample() throws Exception { tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); Map note = new HashMap(); note.put("title", "REST maturity model"); @@ -183,16 +178,17 @@ public void notesCreateExample() throws Exception { ConstrainedFields fields = new ConstrainedFields(NoteInput.class); - this.document.snippets( + this.mockMvc + .perform(post("/notes") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(note))) + .andExpect( + status().isCreated()) + .andDo(this.documentationHandler.document( requestFields( - fields.withPath("title").description("The title of the note"), - fields.withPath("body").description("The body of the note"), - fields.withPath("tags").description("An array of tag resource URIs"))); - - this.mockMvc.perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()); + fields.withPath("title").description("The title of the note"), + fields.withPath("body").description("The body of the note"), + fields.withPath("tags").description("An array of tag resource URIs")))); } @Test @@ -201,11 +197,11 @@ public void noteGetExample() throws Exception { tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); Map note = new HashMap(); note.put("title", "REST maturity model"); @@ -213,27 +209,27 @@ public void noteGetExample() throws Exception { note.put("tags", Arrays.asList(tagLocation)); String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/notes") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); - this.document.snippets( - links( - linkWithRel("self").description("This <>"), - linkWithRel("note-tags").description("This note's <>")), - responseFields( - fieldWithPath("title").description("The title of the note"), - fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources"))); - - this.mockMvc.perform(get(noteLocation)) + this.mockMvc + .perform(get(noteLocation)) .andExpect(status().isOk()) .andExpect(jsonPath("title", is(note.get("title")))) .andExpect(jsonPath("body", is(note.get("body")))) .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))); + .andExpect(jsonPath("_links.note-tags", is(notNullValue()))) + .andDo(this.documentationHandler.document( + links( + linkWithRel("self").description("This <>"), + linkWithRel("note-tags").description("This note's <>")), + responseFields( + fieldWithPath("title").description("The title of the note"), + fieldWithPath("body").description("The body of the note"), + fieldWithPath("_links").description("<> to other resources")))); } @@ -246,12 +242,12 @@ public void tagsListExample() throws Exception { createTag("Hypermedia"); createTag("HTTP"); - this.document.snippets( + this.mockMvc + .perform(get("/tags")) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( responseFields( - fieldWithPath("_embedded.tags").description("An array of <>"))); - - this.mockMvc.perform(get("/tags")) - .andExpect(status().isOk()); + fieldWithPath("_embedded.tags").description("An array of <>")))); } @Test @@ -261,14 +257,14 @@ public void tagsCreateExample() throws Exception { ConstrainedFields fields = new ConstrainedFields(TagInput.class); - this.document.snippets( + this.mockMvc + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andDo(this.documentationHandler.document( requestFields( - fields.withPath("name").description("The name of the tag"))); - - this.mockMvc.perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()); + fields.withPath("name").description("The name of the tag")))); } @Test @@ -278,50 +274,52 @@ public void noteUpdateExample() throws Exception { note.put("body", "http://martinfowler.com/articles/richardsonMaturityModel.html"); String noteLocation = this.mockMvc - .perform( - post("/notes").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(note))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.mockMvc.perform(get(noteLocation)).andExpect(status().isOk()) - .andExpect(jsonPath("title", is(note.get("title")))) - .andExpect(jsonPath("body", is(note.get("body")))) - .andExpect(jsonPath("_links.self.href", is(noteLocation))) - .andExpect(jsonPath("_links.note-tags", is(notNullValue()))); + .perform(post("/notes") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(note))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); + + this.mockMvc + .perform(get(noteLocation)) + .andExpect(status().isOk()) + .andExpect(jsonPath("title", is(note.get("title")))) + .andExpect(jsonPath("body", is(note.get("body")))) + .andExpect(jsonPath("_links.self.href", is(noteLocation))) + .andExpect(jsonPath("_links.note-tags", is(notNullValue()))); Map tag = new HashMap(); tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); Map noteUpdate = new HashMap(); noteUpdate.put("tags", Arrays.asList(tagLocation)); ConstrainedFields fields = new ConstrainedFields(NotePatchInput.class); - - this.document.snippets( + + this.mockMvc + .perform(patch(noteLocation) + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(noteUpdate))) + .andExpect(status().isNoContent()) + .andDo(this.documentationHandler.document( requestFields( - fields.withPath("title") - .description("The title of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("body") - .description("The body of the note") - .type(JsonFieldType.STRING) - .optional(), - fields.withPath("tags") - .description("An array of tag resource URIs"))); - - this.mockMvc.perform( - patch(noteLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(noteUpdate))) - .andExpect(status().isNoContent()); + fields.withPath("title") + .description("The title of the note") + .type(JsonFieldType.STRING) + .optional(), + fields.withPath("body") + .description("The body of the note") + .type(JsonFieldType.STRING) + .optional(), + fields.withPath("tags") + .description("An array of tag resource URIs")))); } @Test @@ -330,23 +328,23 @@ public void tagGetExample() throws Exception { tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); - - this.document.snippets( - links( - linkWithRel("self").description("This <>"), - linkWithRel("tagged-notes").description("The <> that have this tag")), - responseFields( - fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources"))); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); - this.mockMvc.perform(get(tagLocation)) + this.mockMvc + .perform(get(tagLocation)) .andExpect(status().isOk()) - .andExpect(jsonPath("name", is(tag.get("name")))); + .andExpect(jsonPath("name", is(tag.get("name")))) + .andDo(this.documentationHandler.document( + links( + linkWithRel("self").description("This <>"), + linkWithRel("tagged-notes").description("The <> that have this tag")), + responseFields( + fieldWithPath("name").description("The name of the tag"), + fieldWithPath("_links").description("<> to other resources")))); } @Test @@ -355,25 +353,25 @@ public void tagUpdateExample() throws Exception { tag.put("name", "REST"); String tagLocation = this.mockMvc - .perform( - post("/tags").contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tag))) - .andExpect(status().isCreated()).andReturn().getResponse() - .getHeader("Location"); + .perform(post("/tags") + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tag))) + .andExpect(status().isCreated()) + .andReturn().getResponse().getHeader("Location"); Map tagUpdate = new HashMap(); tagUpdate.put("name", "RESTful"); ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class); - this.document.snippets( + this.mockMvc + .perform(patch(tagLocation) + .contentType(MediaTypes.HAL_JSON) + .content(this.objectMapper.writeValueAsString(tagUpdate))) + .andExpect(status().isNoContent()) + .andDo(this.documentationHandler.document( requestFields( - fields.withPath("name").description("The name of the tag"))); - - this.mockMvc.perform( - patch(tagLocation).contentType(MediaTypes.HAL_JSON).content( - this.objectMapper.writeValueAsString(tagUpdate))) - .andExpect(status().isNoContent()); + fields.withPath("name").description("The name of the tag")))); } private void createNote(String title, String body) { From a9305cba8764f5e61a26c64988cb4bab90610671 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Wed, 25 May 2016 12:26:06 +0000 Subject: [PATCH 133/898] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a651766f6..cecc0ca16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.0.2.BUILD-SNAPSHOT +version=1.0.3.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From c1d245f7d438e557217b7633a31e839a3ff4e632 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 May 2016 14:18:55 +0100 Subject: [PATCH 134/898] Add link to restdocs-wiremock to the README Closes gh-244 --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 18fd83473..50f3ebe86 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ There are several that you can contribute to Spring REST Docs: - Ask and answer questions on Stack Overflow using the [`spring-restdocs`][15] tag. - Chat with fellow users [on Gitter][16]. +## Third-party extensions + +| Name | Description | +| ---- | ----------- | +| [restdocs-wiremock][17] | Auto-generate [WireMock][18] stubs as part of documenting your RESTful API | + ## Licence Spring REST Docs is open source software released under the [Apache 2.0 license][14]. @@ -52,4 +58,6 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [13]: CONTRIBUTING.md [14]: http://www.apache.org/licenses/LICENSE-2.0.html [15]: http://stackoverflow.com/tags/spring-restdocs -[16]: https://gitter.im/spring-projects/spring-restdocs \ No newline at end of file +[16]: https://gitter.im/spring-projects/spring-restdocs +[17]: https://github.com/ePages-de/restdocs-wiremock +[18]: http://wiremock.org/ \ No newline at end of file From 64803b5fae42bfb63ed19c7b4467600e66d534c2 Mon Sep 17 00:00:00 2001 From: Jennifer Strater Date: Mon, 30 May 2016 12:22:35 -0500 Subject: [PATCH 135/898] Add Grails sample app Closes gh-250 --- docs/src/docs/asciidoc/getting-started.adoc | 4 + samples/rest-notes-grails/.gitignore | 17 ++ samples/rest-notes-grails/LICENSE | 201 ++++++++++++++++++ samples/rest-notes-grails/README.md | 2 + samples/rest-notes-grails/build.gradle | 79 +++++++ samples/rest-notes-grails/gradle.properties | 2 + .../rest-notes-grails/gradle/restdocs.gradle | 27 +++ .../grails-app/conf/application.yml | 98 +++++++++ .../grails-app/conf/logback.groovy | 23 ++ .../grails-app/conf/spring/resources.groovy | 3 + .../grails-app/controllers/UrlMappings.groovy | 8 + .../com/example/IndexController.groovy | 31 +++ .../InternalServerErrorController.groovy | 11 + .../com/example/NotFoundController.groovy | 11 + .../grails-app/domain/com/example/Note.groovy | 16 ++ .../grails-app/domain/com/example/Tag.groovy | 15 ++ .../grails-app/i18n/messages.properties | 56 +++++ .../grails-app/init/BootStrap.groovy | 14 ++ .../init/com/example/Application.groovy | 10 + samples/rest-notes-grails/src/docs/index.adoc | 148 +++++++++++++ .../com/example/ApiDocumentationSpec.groovy | 146 +++++++++++++ 21 files changed, 922 insertions(+) create mode 100644 samples/rest-notes-grails/.gitignore create mode 100644 samples/rest-notes-grails/LICENSE create mode 100644 samples/rest-notes-grails/README.md create mode 100644 samples/rest-notes-grails/build.gradle create mode 100644 samples/rest-notes-grails/gradle.properties create mode 100644 samples/rest-notes-grails/gradle/restdocs.gradle create mode 100644 samples/rest-notes-grails/grails-app/conf/application.yml create mode 100644 samples/rest-notes-grails/grails-app/conf/logback.groovy create mode 100644 samples/rest-notes-grails/grails-app/conf/spring/resources.groovy create mode 100644 samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy create mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy create mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy create mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy create mode 100644 samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy create mode 100644 samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy create mode 100644 samples/rest-notes-grails/grails-app/i18n/messages.properties create mode 100644 samples/rest-notes-grails/grails-app/init/BootStrap.groovy create mode 100644 samples/rest-notes-grails/grails-app/init/com/example/Application.groovy create mode 100644 samples/rest-notes-grails/src/docs/index.adoc create mode 100644 samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 094c64c29..2a1d161cb 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -18,6 +18,10 @@ If you want to jump straight in, a number of sample applications are available: | Gradle | Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. +| {samples}/rest-notes-grails[Grails] +| Gradle +| Demonstrates the use of Spring REST docs with https://grails.org[Grails] and https://github.com/spockframework/spock[Spock] + | {samples}/rest-notes-slate[Slate] | Gradle | Demonstrates the use of Spring REST Docs with Markdown and diff --git a/samples/rest-notes-grails/.gitignore b/samples/rest-notes-grails/.gitignore new file mode 100644 index 000000000..a521ca89f --- /dev/null +++ b/samples/rest-notes-grails/.gitignore @@ -0,0 +1,17 @@ +Thumbs.db +.DS_Store +.gradle +build/ +classes/ +.idea +*.iml +*.ipr +*.iws +.project +.settings +.classpath +gradlew* +gradle/wrapper + + +src/docs/generated-snippets diff --git a/samples/rest-notes-grails/LICENSE b/samples/rest-notes-grails/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/samples/rest-notes-grails/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/samples/rest-notes-grails/README.md b/samples/rest-notes-grails/README.md new file mode 100644 index 000000000..06c410f79 --- /dev/null +++ b/samples/rest-notes-grails/README.md @@ -0,0 +1,2 @@ +# grails-spring-restdocs-example +Example of adding spring rest docs to sample grails project diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle new file mode 100644 index 000000000..cc8b269ba --- /dev/null +++ b/samples/rest-notes-grails/build.gradle @@ -0,0 +1,79 @@ +buildscript { + ext { + grailsVersion = project.grailsVersion + } + repositories { + mavenLocal() + maven { url "https://repo.grails.org/grails/core" } + maven { url 'https://repo.spring.io/libs-snapshot' } + } + dependencies { + classpath "org.grails:grails-gradle-plugin:$grailsVersion" + classpath "org.grails.plugins:hibernate:4.3.10.5" + classpath 'org.ajoberstar:gradle-git:1.1.0' + } +} + +plugins { + id "io.spring.dependency-management" version "0.5.4.RELEASE" + id 'org.asciidoctor.convert' version '1.5.3' +} + +version "0.1" +group "com.example" + +apply plugin: "spring-boot" +apply plugin: "war" +apply plugin: 'eclipse' +apply plugin: 'idea' +apply plugin: "org.grails.grails-web" + +ext { + grailsVersion = project.grailsVersion + gradleWrapperVersion = project.gradleWrapperVersion +} + +repositories { + mavenLocal() + maven { url "https://repo.grails.org/grails/core" } +} + +dependencyManagement { + imports { + mavenBom "org.grails:grails-bom:$grailsVersion" + } + applyMavenExclusions false +} + +dependencies { + compile "org.springframework.boot:spring-boot-starter-logging" + compile "org.springframework.boot:spring-boot-starter-actuator" + compile "org.springframework.boot:spring-boot-autoconfigure" + compile "org.springframework.boot:spring-boot-starter-tomcat" + compile "org.grails:grails-plugin-url-mappings" + compile "org.grails:grails-plugin-rest" + compile "org.grails:grails-plugin-interceptors" + compile "org.grails:grails-plugin-services" + compile "org.grails:grails-plugin-datasource" + compile "org.grails:grails-plugin-databinding" + compile "org.grails:grails-plugin-async" + compile "org.grails:grails-web-boot" + compile "org.grails:grails-logging" + + compile "org.grails.plugins:hibernate" + compile "org.grails.plugins:cache" + compile "org.hibernate:hibernate-ehcache" + + runtime "com.h2database:h2" + + testCompile "org.grails:grails-plugin-testing" + testCompile "org.grails.plugins:geb" + + console "org.grails:grails-console" +} + +task wrapper(type: Wrapper) { + gradleVersion = gradleWrapperVersion +} + +apply from: 'gradle/restdocs.gradle' diff --git a/samples/rest-notes-grails/gradle.properties b/samples/rest-notes-grails/gradle.properties new file mode 100644 index 000000000..1b1c50f8e --- /dev/null +++ b/samples/rest-notes-grails/gradle.properties @@ -0,0 +1,2 @@ +grailsVersion=3.0.15 +gradleWrapperVersion=2.3 diff --git a/samples/rest-notes-grails/gradle/restdocs.gradle b/samples/rest-notes-grails/gradle/restdocs.gradle new file mode 100644 index 000000000..9b655d2ec --- /dev/null +++ b/samples/rest-notes-grails/gradle/restdocs.gradle @@ -0,0 +1,27 @@ +dependencies { + testCompile 'org.springframework.restdocs:spring-restdocs-restassured:1.1.0.M1' +} + +ext { + snippetsDir = file('src/docs/generated-snippets') +} + +task cleanTempDirs(type: Delete) { + delete fileTree(dir: 'src/docs/generated-snippets') +} + +test { + dependsOn cleanTempDirs + outputs.dir snippetsDir +} + +asciidoctor { + mustRunAfter test + mustRunAfter integrationTest + inputs.dir snippetsDir + sourceDir = file('src/docs') + separateOutputDirs = false + attributes 'snippets': snippetsDir +} + +build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/grails-app/conf/application.yml b/samples/rest-notes-grails/grails-app/conf/application.yml new file mode 100644 index 000000000..6d7d00e8a --- /dev/null +++ b/samples/rest-notes-grails/grails-app/conf/application.yml @@ -0,0 +1,98 @@ +--- +grails: + profile: web-api + codegen: + defaultPackage: com.example +info: + app: + name: '@info.app.name@' + version: '@info.app.version@' + grailsVersion: '@info.app.grailsVersion@' +spring: + groovy: + template: + check-template-location: false + +--- +grails: + mime: + disable: + accept: + header: + userAgents: + - Gecko + - WebKit + - Presto + - Trident + types: + all: '*/*' + atom: application/atom+xml + css: text/css + csv: text/csv + form: application/x-www-form-urlencoded + html: + - text/html + - application/xhtml+xml + js: text/javascript + json: + - application/json + - text/json + multipartForm: multipart/form-data + rss: application/rss+xml + text: text/plain + hal: + - application/hal+json + - application/hal+xml + xml: + - text/xml + - application/xml + urlmapping: + cache: + maxsize: 1000 + controllers: + defaultScope: singleton + converters: + encoding: UTF-8 + hibernate: + cache: + queries: false + +--- +dataSource: + pooled: true + jmxExport: true + driverClassName: org.h2.Driver + username: sa + password: + +environments: + development: + dataSource: + dbCreate: create-drop + url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + test: + dataSource: + dbCreate: update + url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + production: + dataSource: + dbCreate: update + url: jdbc:h2:./prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + properties: + jmxEnabled: true + initialSize: 5 + maxActive: 50 + minIdle: 5 + maxIdle: 25 + maxWait: 10000 + maxAge: 600000 + timeBetweenEvictionRunsMillis: 5000 + minEvictableIdleTimeMillis: 60000 + validationQuery: SELECT 1 + validationQueryTimeout: 3 + validationInterval: 15000 + testOnBorrow: true + testWhileIdle: true + testOnReturn: false + jdbcInterceptors: ConnectionState + defaultTransactionIsolation: 2 # TRANSACTION_READ_COMMITTED diff --git a/samples/rest-notes-grails/grails-app/conf/logback.groovy b/samples/rest-notes-grails/grails-app/conf/logback.groovy new file mode 100644 index 000000000..2f7c41c1a --- /dev/null +++ b/samples/rest-notes-grails/grails-app/conf/logback.groovy @@ -0,0 +1,23 @@ +import grails.util.BuildSettings +import grails.util.Environment + +// See http://logback.qos.ch/manual/groovy.html for details on configuration +appender('STDOUT', ConsoleAppender) { + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } +} + +root(ERROR, ['STDOUT']) + +def targetDir = BuildSettings.TARGET_DIR +if (Environment.isDevelopmentMode() && targetDir) { + appender("FULL_STACKTRACE", FileAppender) { + file = "${targetDir}/stacktrace.log" + append = true + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } + } + logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) +} diff --git a/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy new file mode 100644 index 000000000..fa950068b --- /dev/null +++ b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy @@ -0,0 +1,3 @@ +// Place your Spring DSL code here +beans = { +} diff --git a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy new file mode 100644 index 000000000..98be2a3d3 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy @@ -0,0 +1,8 @@ +class UrlMappings { + + static mappings = { + "/"(controller: 'index') + "500"(controller: 'InternalServerError') + "404"(controller: 'NotFound') + } +} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy new file mode 100644 index 000000000..9ed3bc206 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy @@ -0,0 +1,31 @@ +package com.example + +import grails.core.GrailsApplication +import grails.util.Environment + +class IndexController { + + GrailsApplication grailsApplication + + def index() { + render(contentType: 'application/json') { + message = "Welcome to Grails!" + environment = Environment.current.name + appversion = grailsApplication.metadata['info.app.version'] + grailsversion = grailsApplication.metadata['info.app.grailsVersion'] + appprofile = grailsApplication.config.grails?.profile + groovyversion = GroovySystem.getVersion() + jvmversion = System.getProperty('java.version') + controllers = array { + for (c in grailsApplication.controllerClasses) { + controller([name: c.fullName]) + } + } + plugins = array { + for (p in grailsApplication.mainContext.pluginManager.allPlugins) { + plugin([name: p.fullName]) + } + } + } + } +} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy new file mode 100644 index 000000000..d5c169010 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy @@ -0,0 +1,11 @@ +package com.example + +class InternalServerErrorController { + + def index() { + render(contentType: 'application/json') { + error = 500 + message = "Internal server error" + } + } +} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy new file mode 100644 index 000000000..8527921de --- /dev/null +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy @@ -0,0 +1,11 @@ +package com.example + +class NotFoundController { + + def index() { + render(contentType: 'application/json') { + error = 404 + message = "Not Found" + } + } +} diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy new file mode 100644 index 000000000..d13fbff54 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy @@ -0,0 +1,16 @@ +package com.example + +import grails.rest.Resource + +@Resource(uri='/notes', formats = ['json', 'xml']) +class Note { + Long id + String title + String body + + static hasMany = [tags: Tag] + static mapping = { + tags joinTable: [name: "mm_notes_tags", key: 'mm_note_id' ] + } +} + diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy new file mode 100644 index 000000000..f9b33d116 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy @@ -0,0 +1,15 @@ +package com.example + +import grails.rest.Resource + +@Resource(uri='/tags', formats = ['json', 'xml']) +class Tag { + Long id + String name + + static hasMany = [notes: Note] + static belongsTo = Note + static mapping = { + notes joinTable: [name: "mm_notes_tags", key: 'mm_tag_id'] + } +} diff --git a/samples/rest-notes-grails/grails-app/i18n/messages.properties b/samples/rest-notes-grails/grails-app/i18n/messages.properties new file mode 100644 index 000000000..b04513621 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/i18n/messages.properties @@ -0,0 +1,56 @@ +default.doesnt.match.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}] +default.invalid.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL +default.invalid.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number +default.invalid.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address +default.invalid.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range from [{3}] to [{4}] +default.invalid.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size range from [{3}] to [{4}] +default.invalid.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}] +default.invalid.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}] +default.invalid.max.size.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}] +default.invalid.min.size.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}] +default.invalid.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation +default.not.inlist.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}] +default.blank.message=Property [{0}] of class [{1}] cannot be blank +default.not.equal.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}] +default.null.message=Property [{0}] of class [{1}] cannot be null +default.not.unique.message=Property [{0}] of class [{1}] with value [{2}] must be unique + +default.paginate.prev=Previous +default.paginate.next=Next +default.boolean.true=True +default.boolean.false=False +default.date.format=yyyy-MM-dd HH:mm:ss z +default.number.format=0 + +default.created.message={0} {1} created +default.updated.message={0} {1} updated +default.deleted.message={0} {1} deleted +default.not.deleted.message={0} {1} could not be deleted +default.not.found.message={0} not found with id {1} +default.optimistic.locking.failure=Another user has updated this {0} while you were editing + +default.home.label=Home +default.list.label={0} List +default.add.label=Add {0} +default.new.label=New {0} +default.create.label=Create {0} +default.show.label=Show {0} +default.edit.label=Edit {0} + +default.button.create.label=Create +default.button.edit.label=Edit +default.button.update.label=Update +default.button.delete.label=Delete +default.button.delete.confirm.message=Are you sure? + +# Data binding errors. Use "typeMismatch.$className.$propertyName to customize (eg typeMismatch.Book.author) +typeMismatch.java.net.URL=Property {0} must be a valid URL +typeMismatch.java.net.URI=Property {0} must be a valid URI +typeMismatch.java.util.Date=Property {0} must be a valid Date +typeMismatch.java.lang.Double=Property {0} must be a valid number +typeMismatch.java.lang.Integer=Property {0} must be a valid number +typeMismatch.java.lang.Long=Property {0} must be a valid number +typeMismatch.java.lang.Short=Property {0} must be a valid number +typeMismatch.java.math.BigDecimal=Property {0} must be a valid number +typeMismatch.java.math.BigInteger=Property {0} must be a valid number +typeMismatch=Property {0} is type-mismatched diff --git a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy new file mode 100644 index 000000000..1fba80026 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy @@ -0,0 +1,14 @@ +import com.example.Note + +class BootStrap { + + def init = { servletContext -> + environments { + test { + new Note(title: 'Hello, World!', body: 'Hello from the Integration Test').save() + } + } + } + def destroy = { + } +} diff --git a/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy new file mode 100644 index 000000000..58ad55977 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy @@ -0,0 +1,10 @@ +package com.example + +import grails.boot.GrailsApp +import grails.boot.config.GrailsAutoConfiguration + +class Application extends GrailsAutoConfiguration { + static void main(String[] args) { + GrailsApp.run(Application, args) + } +} diff --git a/samples/rest-notes-grails/src/docs/index.adoc b/samples/rest-notes-grails/src/docs/index.adoc new file mode 100644 index 000000000..231af2ecf --- /dev/null +++ b/samples/rest-notes-grails/src/docs/index.adoc @@ -0,0 +1,148 @@ += Grails RESTful Notes API Guide +Andy Wilkinson; Jenn Strater +:doctype: book +:icons: font +:source-highlighter: highlightjs +:toc: left +:toclevels: 4 +:sectlinks: + +[[overview]] += Overview + +[[overview-http-verbs]] +== HTTP verbs + +Grails RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP verbs. + +|=== +| Verb | Usage + +| `GET` +| Used to retrieve a resource + +| `POST` +| Used to create a new resource + +| `PATCH` +| Used to update an existing resource, including partial updates + +| `DELETE` +| Used to delete an existing resource +|=== + +[[overview-http-status-codes]] +== HTTP status codes + +Grails RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its +use of HTTP status codes. + +|=== +| Status code | Usage + +| `200 OK` +| The request completed successfully + +| `201 Created` +| A new resource has been created successfully. The resource's URI is available from the response's +`Location` header + +| `204 No Content` +| An update to an existing resource has been applied successfully + +| `400 Bad Request` +| The request was malformed. The response body will include an error providing further information + +| `404 Not Found` +| The requested resource did not exist +|=== + +[[resources]] += Resources + + +[[resources-index]] +== Index + +The index provides the entry point into the service. + + + +[[resources-index-access]] +=== Accessing the index + +A `GET` request is used to access the index + +==== Example request + +include::{snippets}/index-example/curl-request.adoc[] + +==== Response structure + +include::{snippets}/index-example/response-fields.adoc[] + +==== Example response + +include::{snippets}/index-example/http-response.adoc[] + + +[[resources-notes]] +== Notes + +The Notes resources is used to create and list notes + + + +[[resources-notes-list]] +=== Listing notes + +A `GET` request will list all of the service's notes. + +==== Response structure + +include::{snippets}/notes-list-example/response-fields.adoc[] + +==== Example request + +include::{snippets}/notes-list-example/curl-request.adoc[] + +==== Example response + +include::{snippets}/notes-list-example/http-response.adoc[] + + + +[[resources-notes-create]] +=== Creating a note + +A `POST` request is used to create a note + +==== Request structure + +include::{snippets}/notes-create-example/request-fields.adoc[] + +==== Example request + +include::{snippets}/notes-create-example/curl-request.adoc[] + +==== Example response + +include::{snippets}/notes-create-example/http-response.adoc[] + +[[resources-note-retrieve]] +=== Retrieve a note + +A `GET` request will retrieve the details of a note + +==== Response structure + +include::{snippets}/note-get-example/response-fields.adoc[] + +==== Example request + +include::{snippets}/note-get-example/curl-request.adoc[] + +==== Example response + +include::{snippets}/note-get-example/http-response.adoc[] diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy new file mode 100644 index 000000000..949f8a568 --- /dev/null +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -0,0 +1,146 @@ +package com.example + +import org.springframework.restdocs.payload.JsonFieldType + +import static com.jayway.restassured.RestAssured.given +import static org.hamcrest.CoreMatchers.is +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest +import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint +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.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration + +import com.jayway.restassured.builder.RequestSpecBuilder +import com.jayway.restassured.specification.RequestSpecification +import grails.test.mixin.integration.Integration +import grails.transaction.Rollback +import org.junit.Rule +import org.springframework.restdocs.JUnitRestDocumentation +import org.springframework.http.MediaType +import spock.lang.Specification + +@Integration +@Rollback +class ApiDocumentationSpec extends Specification { + @Rule + JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') + + protected RequestSpecification documentationSpec + + void setup() { + this.documentationSpec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation)) + .build() + } + + void 'test and document get request for /index'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('index-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('message').description('Welcome to Grails!'), + fieldWithPath('environment').description("The running environment"), + fieldWithPath('appversion').description('version of the app that is running'), + fieldWithPath('grailsversion').description('the version of grails used in this project'), + fieldWithPath('appprofile').description('the profile of grails used in this project'), + fieldWithPath('groovyversion').description('the version of groovy used in this project'), + fieldWithPath('jvmversion').description('the version of the jvm used in this project'), + fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), + fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), + ))) + .when() + .port(8080) + .get('/') + .then() + .assertThat() + .statusCode(is(200)) + } + + void 'test and document notes list request'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-list-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('[].class').description('the class of the resource'), + fieldWithPath('[].id').description('the id of the note'), + fieldWithPath('[].title').description('the title of the note'), + fieldWithPath('[].body').description('the body of the note'), + fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes') + .then() + .assertThat() + .statusCode(is(200)) + } + + void 'test and document create new note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .contentType(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-create-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + requestFields( + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') + ), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') + .when() + .port(8080) + .post('/notes') + .then() + .assertThat() + .statusCode(is(201)) + } + + void 'test and document getting specific note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('note-get-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes/1') + .then() + .assertThat() + .statusCode(is(200)) + } +} From 34ec9d523d69a80d7d51d02b25b17a7c9495bcd8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 09:52:26 +0100 Subject: [PATCH 136/898] Polish "Add Grails sample app" (1338df9) The sole structural change is to rearrange the build slightly so that, like the other samples, the version of the Grails sample's REST Docs dependency is automatically updated. See gh-250 --- build.gradle | 5 + .../build/SampleBuildConfigurer.groovy | 3 + samples/rest-notes-grails/LICENSE | 201 --------------- samples/rest-notes-grails/README.md | 2 +- samples/rest-notes-grails/build.gradle | 126 +++++---- .../rest-notes-grails/gradle/restdocs.gradle | 27 -- .../grails-app/conf/logback.groovy | 38 ++- .../grails-app/conf/spring/resources.groovy | 3 +- .../grails-app/controllers/UrlMappings.groovy | 27 +- .../com/example/IndexController.groovy | 61 +++-- .../InternalServerErrorController.groovy | 29 ++- .../com/example/NotFoundController.groovy | 29 ++- .../grails-app/domain/com/example/Note.groovy | 37 ++- .../grails-app/domain/com/example/Tag.groovy | 37 ++- .../grails-app/init/BootStrap.groovy | 35 ++- .../init/com/example/Application.groovy | 24 +- .../com/example/ApiDocumentationSpec.groovy | 240 ++++++++++-------- 17 files changed, 455 insertions(+), 469 deletions(-) delete mode 100644 samples/rest-notes-grails/LICENSE delete mode 100644 samples/rest-notes-grails/gradle/restdocs.gradle diff --git a/build.gradle b/build.gradle index 38edf1e73..af4d5bdd4 100644 --- a/build.gradle +++ b/build.gradle @@ -163,6 +163,10 @@ samples { dependOn 'spring-restdocs-mockmvc:install' dependOn 'spring-restdocs-restassured:install' + restNotesGrails { + workingDir "$projectDir/samples/rest-notes-grails" + } + restNotesSpringHateoas { workingDir "$projectDir/samples/rest-notes-spring-hateoas" } @@ -183,6 +187,7 @@ samples { workingDir "$projectDir/samples/rest-notes-slate" build false } + } task api (type: Javadoc) { diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 1be930cf8..69b6f1fd5 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -61,6 +61,9 @@ public class SampleBuildConfigurer { replaceVersion(new File(this.workingDir, 'build.gradle'), "ext\\['spring-restdocs.version'\\] = '.*'", "ext['spring-restdocs.version'] = '${project.version}'") + replaceVersion(new File(this.workingDir, 'build.gradle'), + "restDocsVersion = \".*\"", + "restDocsVersion = \"${project.version}\"") } } else if (new File(sampleDir, 'pom.xml').isFile()) { diff --git a/samples/rest-notes-grails/LICENSE b/samples/rest-notes-grails/LICENSE deleted file mode 100644 index 8dada3eda..000000000 --- a/samples/rest-notes-grails/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - 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. diff --git a/samples/rest-notes-grails/README.md b/samples/rest-notes-grails/README.md index 06c410f79..386292af4 100644 --- a/samples/rest-notes-grails/README.md +++ b/samples/rest-notes-grails/README.md @@ -1,2 +1,2 @@ # grails-spring-restdocs-example -Example of adding spring rest docs to sample grails project +Example of using Spring REST Docs with Grails diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index cc8b269ba..85ac7c4e6 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -1,22 +1,22 @@ buildscript { - ext { - grailsVersion = project.grailsVersion - } - repositories { - mavenLocal() - maven { url "https://repo.grails.org/grails/core" } - maven { url 'https://repo.spring.io/libs-snapshot' } - } - dependencies { - classpath "org.grails:grails-gradle-plugin:$grailsVersion" - classpath "org.grails.plugins:hibernate:4.3.10.5" - classpath 'org.ajoberstar:gradle-git:1.1.0' - } + ext { + grailsVersion = project.grailsVersion + } + repositories { + mavenLocal() + maven { url "https://repo.grails.org/grails/core" } + maven { url 'https://repo.spring.io/libs-snapshot' } + } + dependencies { + classpath "org.grails:grails-gradle-plugin:$grailsVersion" + classpath "org.grails.plugins:hibernate:4.3.10.5" + classpath 'org.ajoberstar:gradle-git:1.1.0' + } } plugins { - id "io.spring.dependency-management" version "0.5.4.RELEASE" - id 'org.asciidoctor.convert' version '1.5.3' + id "io.spring.dependency-management" version "0.5.4.RELEASE" + id 'org.asciidoctor.convert' version '1.5.3' } version "0.1" @@ -29,51 +29,79 @@ apply plugin: 'idea' apply plugin: "org.grails.grails-web" ext { - grailsVersion = project.grailsVersion - gradleWrapperVersion = project.gradleWrapperVersion + grailsVersion = project.grailsVersion + gradleWrapperVersion = project.gradleWrapperVersion + restDocsVersion = "1.1.0.BUILD-SNAPSHOT" + snippetsDir = file('src/docs/generated-snippets') } repositories { - mavenLocal() - maven { url "https://repo.grails.org/grails/core" } + mavenLocal() + maven { url 'https://repo.spring.io/libs-snapshot' } + maven { url "https://repo.grails.org/grails/core" } } dependencyManagement { - imports { - mavenBom "org.grails:grails-bom:$grailsVersion" - } - applyMavenExclusions false + dependencies { + dependency "org.springframework.restdocs:spring-restdocs-restassured:$restDocsVersion" + } + imports { + mavenBom "org.grails:grails-bom:$grailsVersion" + } + applyMavenExclusions false } dependencies { - compile "org.springframework.boot:spring-boot-starter-logging" - compile "org.springframework.boot:spring-boot-starter-actuator" - compile "org.springframework.boot:spring-boot-autoconfigure" - compile "org.springframework.boot:spring-boot-starter-tomcat" - compile "org.grails:grails-plugin-url-mappings" - compile "org.grails:grails-plugin-rest" - compile "org.grails:grails-plugin-interceptors" - compile "org.grails:grails-plugin-services" - compile "org.grails:grails-plugin-datasource" - compile "org.grails:grails-plugin-databinding" - compile "org.grails:grails-plugin-async" - compile "org.grails:grails-web-boot" - compile "org.grails:grails-logging" - - compile "org.grails.plugins:hibernate" - compile "org.grails.plugins:cache" - compile "org.hibernate:hibernate-ehcache" - - runtime "com.h2database:h2" - - testCompile "org.grails:grails-plugin-testing" - testCompile "org.grails.plugins:geb" - - console "org.grails:grails-console" + compile "org.springframework.boot:spring-boot-starter-logging" + compile "org.springframework.boot:spring-boot-starter-actuator" + compile "org.springframework.boot:spring-boot-autoconfigure" + compile "org.springframework.boot:spring-boot-starter-tomcat" + compile "org.grails:grails-plugin-url-mappings" + compile "org.grails:grails-plugin-rest" + compile "org.grails:grails-plugin-interceptors" + compile "org.grails:grails-plugin-services" + compile "org.grails:grails-plugin-datasource" + compile "org.grails:grails-plugin-databinding" + compile "org.grails:grails-plugin-async" + compile "org.grails:grails-web-boot" + compile "org.grails:grails-logging" + + compile "org.grails.plugins:hibernate" + compile "org.grails.plugins:cache" + compile "org.hibernate:hibernate-ehcache" + + runtime "com.h2database:h2" + + testCompile "org.grails:grails-plugin-testing" + testCompile "org.grails.plugins:geb" + testCompile 'org.springframework.restdocs:spring-restdocs-restassured' + + console "org.grails:grails-console" } task wrapper(type: Wrapper) { - gradleVersion = gradleWrapperVersion + gradleVersion = gradleWrapperVersion +} + +ext { + snippetsDir = file('src/docs/generated-snippets') +} + +task cleanTempDirs(type: Delete) { + delete fileTree(dir: 'src/docs/generated-snippets') +} + +test { + dependsOn cleanTempDirs + outputs.dir snippetsDir +} + +asciidoctor { + dependsOn integrationTest + inputs.dir snippetsDir + sourceDir = file('src/docs') + separateOutputDirs = false + attributes 'snippets': snippetsDir } -apply from: 'gradle/restdocs.gradle' +build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/gradle/restdocs.gradle b/samples/rest-notes-grails/gradle/restdocs.gradle deleted file mode 100644 index 9b655d2ec..000000000 --- a/samples/rest-notes-grails/gradle/restdocs.gradle +++ /dev/null @@ -1,27 +0,0 @@ -dependencies { - testCompile 'org.springframework.restdocs:spring-restdocs-restassured:1.1.0.M1' -} - -ext { - snippetsDir = file('src/docs/generated-snippets') -} - -task cleanTempDirs(type: Delete) { - delete fileTree(dir: 'src/docs/generated-snippets') -} - -test { - dependsOn cleanTempDirs - outputs.dir snippetsDir -} - -asciidoctor { - mustRunAfter test - mustRunAfter integrationTest - inputs.dir snippetsDir - sourceDir = file('src/docs') - separateOutputDirs = false - attributes 'snippets': snippetsDir -} - -build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/grails-app/conf/logback.groovy b/samples/rest-notes-grails/grails-app/conf/logback.groovy index 2f7c41c1a..c8bc3a402 100644 --- a/samples/rest-notes-grails/grails-app/conf/logback.groovy +++ b/samples/rest-notes-grails/grails-app/conf/logback.groovy @@ -1,23 +1,39 @@ +/* + * Copyright 2014-2016 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. + */ + import grails.util.BuildSettings import grails.util.Environment // See http://logback.qos.ch/manual/groovy.html for details on configuration appender('STDOUT', ConsoleAppender) { - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } } root(ERROR, ['STDOUT']) def targetDir = BuildSettings.TARGET_DIR if (Environment.isDevelopmentMode() && targetDir) { - appender("FULL_STACKTRACE", FileAppender) { - file = "${targetDir}/stacktrace.log" - append = true - encoder(PatternLayoutEncoder) { - pattern = "%level %logger - %msg%n" - } - } - logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) + appender("FULL_STACKTRACE", FileAppender) { + file = "${targetDir}/stacktrace.log" + append = true + encoder(PatternLayoutEncoder) { + pattern = "%level %logger - %msg%n" + } + } + logger("StackTrace", ERROR, ['FULL_STACKTRACE'], false) } diff --git a/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy index fa950068b..4907ee437 100644 --- a/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy +++ b/samples/rest-notes-grails/grails-app/conf/spring/resources.groovy @@ -1,3 +1,2 @@ // Place your Spring DSL code here -beans = { -} +beans = {} diff --git a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy index 98be2a3d3..7d8cffe56 100644 --- a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy @@ -1,8 +1,25 @@ +/* + * Copyright 2014-2016 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. + */ + class UrlMappings { - static mappings = { - "/"(controller: 'index') - "500"(controller: 'InternalServerError') - "404"(controller: 'NotFound') - } + static mappings = { + "/"(controller: 'index') + "500"(controller: 'InternalServerError') + "404"(controller: 'NotFound') + } + } diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy index 9ed3bc206..01f585182 100644 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy @@ -1,3 +1,19 @@ +/* + * Copyright 2014-2016 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 com.example import grails.core.GrailsApplication @@ -5,27 +21,28 @@ import grails.util.Environment class IndexController { - GrailsApplication grailsApplication + GrailsApplication grailsApplication + + def index() { + render(contentType: 'application/json') { + message = "Welcome to Grails!" + environment = Environment.current.name + appversion = grailsApplication.metadata['info.app.version'] + grailsversion = grailsApplication.metadata['info.app.grailsVersion'] + appprofile = grailsApplication.config.grails?.profile + groovyversion = GroovySystem.getVersion() + jvmversion = System.getProperty('java.version') + controllers = array { + for (c in grailsApplication.controllerClasses) { + controller([name: c.fullName]) + } + } + plugins = array { + for (p in grailsApplication.mainContext.pluginManager.allPlugins) { + plugin([name: p.fullName]) + } + } + } + } - def index() { - render(contentType: 'application/json') { - message = "Welcome to Grails!" - environment = Environment.current.name - appversion = grailsApplication.metadata['info.app.version'] - grailsversion = grailsApplication.metadata['info.app.grailsVersion'] - appprofile = grailsApplication.config.grails?.profile - groovyversion = GroovySystem.getVersion() - jvmversion = System.getProperty('java.version') - controllers = array { - for (c in grailsApplication.controllerClasses) { - controller([name: c.fullName]) - } - } - plugins = array { - for (p in grailsApplication.mainContext.pluginManager.allPlugins) { - plugin([name: p.fullName]) - } - } - } - } } diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy index d5c169010..c0e80312c 100644 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy @@ -1,11 +1,28 @@ +/* + * Copyright 2014-2016 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 com.example class InternalServerErrorController { - def index() { - render(contentType: 'application/json') { - error = 500 - message = "Internal server error" - } - } + def index() { + render(contentType: 'application/json') { + error = 500 + message = "Internal server error" + } + } + } diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy index 8527921de..039968a93 100644 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy @@ -1,11 +1,28 @@ +/* + * Copyright 2014-2016 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 com.example class NotFoundController { - def index() { - render(contentType: 'application/json') { - error = 404 - message = "Not Found" - } - } + def index() { + render(contentType: 'application/json') { + error = 404 + message = "Not Found" + } + } + } diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy index d13fbff54..140b839cd 100644 --- a/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Note.groovy @@ -1,16 +1,37 @@ +/* + * Copyright 2014-2016 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 com.example import grails.rest.Resource @Resource(uri='/notes', formats = ['json', 'xml']) class Note { - Long id - String title - String body - - static hasMany = [tags: Tag] - static mapping = { - tags joinTable: [name: "mm_notes_tags", key: 'mm_note_id' ] - } + + Long id + + String title + + String body + + static hasMany = [tags: Tag] + + static mapping = { + tags joinTable: [name: "mm_notes_tags", key: 'mm_note_id' ] + } + } diff --git a/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy index f9b33d116..b0c712c63 100644 --- a/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy +++ b/samples/rest-notes-grails/grails-app/domain/com/example/Tag.groovy @@ -1,15 +1,36 @@ +/* + * Copyright 2014-2016 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 com.example import grails.rest.Resource @Resource(uri='/tags', formats = ['json', 'xml']) class Tag { - Long id - String name - - static hasMany = [notes: Note] - static belongsTo = Note - static mapping = { - notes joinTable: [name: "mm_notes_tags", key: 'mm_tag_id'] - } + + Long id + + String name + + static hasMany = [notes: Note] + + static belongsTo = Note + + static mapping = { + notes joinTable: [name: "mm_notes_tags", key: 'mm_tag_id'] + } + } diff --git a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy index 1fba80026..0efbc954f 100644 --- a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy +++ b/samples/rest-notes-grails/grails-app/init/BootStrap.groovy @@ -1,14 +1,31 @@ +/* + * Copyright 2014-2016 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. + */ + import com.example.Note class BootStrap { - def init = { servletContext -> - environments { - test { - new Note(title: 'Hello, World!', body: 'Hello from the Integration Test').save() - } - } - } - def destroy = { - } + def init = { servletContext -> + environments { + test { + new Note(title: 'Hello, World!', body: 'Hello from the Integration Test').save() + } + } + } + + def destroy = {} + } diff --git a/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy index 58ad55977..6da2f620d 100644 --- a/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy +++ b/samples/rest-notes-grails/grails-app/init/com/example/Application.groovy @@ -1,10 +1,28 @@ +/* + * Copyright 2014-2016 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 com.example import grails.boot.GrailsApp import grails.boot.config.GrailsAutoConfiguration class Application extends GrailsAutoConfiguration { - static void main(String[] args) { - GrailsApp.run(Application, args) - } + + static void main(String[] args) { + GrailsApp.run(Application, args) + } + } diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index 949f8a568..ae594901e 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -1,3 +1,19 @@ +/* + * Copyright 2014-2016 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 com.example import org.springframework.restdocs.payload.JsonFieldType @@ -26,121 +42,123 @@ import spock.lang.Specification @Integration @Rollback class ApiDocumentationSpec extends Specification { - @Rule - JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') - protected RequestSpecification documentationSpec + @Rule + JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') + + protected RequestSpecification documentationSpec + + void setup() { + this.documentationSpec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation)) + .build() + } - void setup() { - this.documentationSpec = new RequestSpecBuilder() - .addFilter(documentationConfiguration(restDocumentation)) - .build() - } + void 'test and document get request for /index'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('index-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('message').description('Welcome to Grails!'), + fieldWithPath('environment').description("The running environment"), + fieldWithPath('appversion').description('version of the app that is running'), + fieldWithPath('grailsversion').description('the version of grails used in this project'), + fieldWithPath('appprofile').description('the profile of grails used in this project'), + fieldWithPath('groovyversion').description('the version of groovy used in this project'), + fieldWithPath('jvmversion').description('the version of the jvm used in this project'), + fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), + fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), + ))) + .when() + .port(8080) + .get('/') + .then() + .assertThat() + .statusCode(is(200)) + } - void 'test and document get request for /index'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('index-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('message').description('Welcome to Grails!'), - fieldWithPath('environment').description("The running environment"), - fieldWithPath('appversion').description('version of the app that is running'), - fieldWithPath('grailsversion').description('the version of grails used in this project'), - fieldWithPath('appprofile').description('the profile of grails used in this project'), - fieldWithPath('groovyversion').description('the version of groovy used in this project'), - fieldWithPath('jvmversion').description('the version of the jvm used in this project'), - fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), - fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), - ))) - .when() - .port(8080) - .get('/') - .then() - .assertThat() - .statusCode(is(200)) - } + void 'test and document notes list request'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-list-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('[].class').description('the class of the resource'), + fieldWithPath('[].id').description('the id of the note'), + fieldWithPath('[].title').description('the title of the note'), + fieldWithPath('[].body').description('the body of the note'), + fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes') + .then() + .assertThat() + .statusCode(is(200)) + } - void 'test and document notes list request'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('notes-list-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('[].class').description('the class of the resource'), - fieldWithPath('[].id').description('the id of the note'), - fieldWithPath('[].title').description('the title of the note'), - fieldWithPath('[].body').description('the body of the note'), - fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), - ))) - .when() - .port(8080) - .get('/notes') - .then() - .assertThat() - .statusCode(is(200)) - } + void 'test and document create new note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .contentType(MediaType.APPLICATION_JSON.toString()) + .filter(document('notes-create-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + requestFields( + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') + ), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') + .when() + .port(8080) + .post('/notes') + .then() + .assertThat() + .statusCode(is(201)) + } - void 'test and document create new note'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .contentType(MediaType.APPLICATION_JSON.toString()) - .filter(document('notes-create-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - requestFields( - fieldWithPath('title').description('the title of the note'), - fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') - ), - responseFields( - fieldWithPath('class').description('the class of the resource'), - fieldWithPath('id').description('the id of the note'), - fieldWithPath('title').description('the title of the note'), - fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), - ))) - .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') - .when() - .port(8080) - .post('/notes') - .then() - .assertThat() - .statusCode(is(201)) - } + void 'test and document getting specific note'() { + expect: + given(this.documentationSpec) + .accept(MediaType.APPLICATION_JSON.toString()) + .filter(document('note-get-example', + preprocessRequest(modifyUris() + .host('api.example.com') + .removePort()), + preprocessResponse(prettyPrint()), + responseFields( + fieldWithPath('class').description('the class of the resource'), + fieldWithPath('id').description('the id of the note'), + fieldWithPath('title').description('the title of the note'), + fieldWithPath('body').description('the body of the note'), + fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + ))) + .when() + .port(8080) + .get('/notes/1') + .then() + .assertThat() + .statusCode(is(200)) + } - void 'test and document getting specific note'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('note-get-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('class').description('the class of the resource'), - fieldWithPath('id').description('the id of the note'), - fieldWithPath('title').description('the title of the note'), - fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), - ))) - .when() - .port(8080) - .get('/notes/1') - .then() - .assertThat() - .statusCode(is(200)) - } } From 4f87f55aaa5d33555976c5c11e5d189421a7e929 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 10:04:30 +0100 Subject: [PATCH 137/898] Fix link to Spring Framework javadoc --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index af4d5bdd4..e917b7b9e 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ ext { springVersion = '4.2.5.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', - 'http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/', + "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", 'https://docs.jboss.org/hibernate/stable/beanvalidation/api/', 'https://docs.jboss.org/hibernate/stable/validator/api/' ] as String[] From d5a2dff210d92730abafe0058c027264859514c3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 10:07:19 +0100 Subject: [PATCH 138/898] Update the samples to use Spring Boot 1.3.5.RELEASE --- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- spring-restdocs-restassured/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index f4c79f9b2..7b1ed764e 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' } } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index d30ca93ee..c91372f9d 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' } } diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 32b12dc4b..824717455 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.3.3.RELEASE + 1.3.5.RELEASE diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 7941829f9..845bf967a 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' } } diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 7683e955d..4e027aa85 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' } } diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index c9385ea82..b5a9f46c8 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -10,7 +10,7 @@ dependencies { testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.3.RELEASE' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.5.RELEASE' testCompile 'org.springframework:spring-test' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') testRuntime 'commons-logging:commons-logging:1.2' From 686914071a38c2d25afb230fe88ecdaf111b16bc Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 10:20:19 +0100 Subject: [PATCH 139/898] Configure Grails sample to run tests using a random server port See gh-250 --- .../grails-app/conf/application.yml | 2 ++ .../groovy/com/example/ApiDocumentationSpec.groovy | 14 +++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/samples/rest-notes-grails/grails-app/conf/application.yml b/samples/rest-notes-grails/grails-app/conf/application.yml index 6d7d00e8a..3fe5b2978 100644 --- a/samples/rest-notes-grails/grails-app/conf/application.yml +++ b/samples/rest-notes-grails/grails-app/conf/application.yml @@ -74,6 +74,8 @@ environments: dataSource: dbCreate: update url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE + server: + port: 0 production: dataSource: dbCreate: update diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index ae594901e..3f0da9cae 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -35,8 +35,9 @@ import com.jayway.restassured.specification.RequestSpecification import grails.test.mixin.integration.Integration import grails.transaction.Rollback import org.junit.Rule -import org.springframework.restdocs.JUnitRestDocumentation +import org.springframework.beans.factory.annotation.Value import org.springframework.http.MediaType +import org.springframework.restdocs.JUnitRestDocumentation import spock.lang.Specification @Integration @@ -46,6 +47,9 @@ class ApiDocumentationSpec extends Specification { @Rule JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') + @Value('${local.server.port}') + Integer serverPort + protected RequestSpecification documentationSpec void setup() { @@ -75,7 +79,7 @@ class ApiDocumentationSpec extends Specification { fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), ))) .when() - .port(8080) + .port(this.serverPort) .get('/') .then() .assertThat() @@ -99,7 +103,7 @@ class ApiDocumentationSpec extends Specification { fieldWithPath('[].tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), ))) .when() - .port(8080) + .port(this.serverPort) .get('/notes') .then() .assertThat() @@ -130,7 +134,7 @@ class ApiDocumentationSpec extends Specification { ))) .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') .when() - .port(8080) + .port(this.serverPort) .post('/notes') .then() .assertThat() @@ -154,7 +158,7 @@ class ApiDocumentationSpec extends Specification { fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), ))) .when() - .port(8080) + .port(this.serverPort) .get('/notes/1') .then() .assertThat() From b804ebbe5ac7f5a5972260e3b363365b338a84de Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 May 2016 11:35:43 +0100 Subject: [PATCH 140/898] Add a description to the REST Assured module --- spring-restdocs-restassured/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index b5a9f46c8..ed8fca51a 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,3 +1,5 @@ +description = 'Spring REST Docs REST Assured' + boolean isPlatformUsingBootOneFour() { def bootVersion = dependencyManagement.springIoTestRuntime.managedVersions['org.springframework.boot:spring-boot'] bootVersion.startsWith('1.4') From 832f4fdbc3429442629533f5c9cf7d5940557340 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Tue, 31 May 2016 10:53:12 +0000 Subject: [PATCH 141/898] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4797e2a66..179a34021 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.0.BUILD-SNAPSHOT +version=1.1.1.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From 41b7fd63f61ccaa80028ddbc1c5a71f277ed7752 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 16 Jun 2016 13:13:44 +0100 Subject: [PATCH 142/898] Fix RestAssuredRestDocumentationConfigurerTests.nextFilterIsCalled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test wasn’t actually verifying that the next filter was being called. This commit corrects that. --- .../RestAssuredRestDocumentationConfigurerTests.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index d60b85ff3..3417fbbaf 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -35,7 +35,6 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.Matchers.hasEntry; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -65,8 +64,7 @@ public class RestAssuredRestDocumentationConfigurerTests { @Test public void nextFilterIsCalled() { this.configurer.filter(this.requestSpec, this.responseSpec, this.filterContext); - verify(this.filterContext).setValue( - eq(RestDocumentationFilter.CONTEXT_KEY_CONFIGURATION), any(Map.class)); + verify(this.filterContext).next(this.requestSpec, this.responseSpec); } @Test From 857525954befbf4350999d4f6364e1ec96697eb0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jun 2016 15:41:22 +0100 Subject: [PATCH 143/898] Update samples following the release of 1.1.0.RELEASE --- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-grails/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 7b1ed764e..fa089b3ec 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 85ac7c4e6..3d425282b 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -31,7 +31,7 @@ apply plugin: "org.grails.grails-web" ext { grailsVersion = project.grailsVersion gradleWrapperVersion = project.gradleWrapperVersion - restDocsVersion = "1.1.0.BUILD-SNAPSHOT" + restDocsVersion = "1.1.1.BUILD-SNAPSHOT" snippetsDir = file('src/docs/generated-snippets') } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index c91372f9d..ae6b3f644 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -26,7 +26,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 824717455..eb5b467c3 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.1.0.BUILD-SNAPSHOT + 1.1.1.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 845bf967a..fdde83881 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 4e027aa85..05c7eea82 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -29,7 +29,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.0.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' From a4298b29e7f30715c7d8f96334c8d30a02e541a1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jun 2016 13:10:04 +0100 Subject: [PATCH 144/898] =?UTF-8?q?Include=20request=20URI=E2=80=99s=20por?= =?UTF-8?q?t,=20if=20any,=20in=20default=20Host=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, OperationRequestFactory would add a Host header if one did not already exist in the request’s headers, however it did not include the port. This meant that when the request was being made to a non-standard port (a port other than 80 for an HTTP request and 443 for an HTTPS request) the Host header was incorrect. This commit updates OperationRequestFactory to check the URI for a port and, if it has one, include it in the Host header. UriModifyingOperationPreprocessor has also been updated to correctly include the port in the Host header. Closes gh-269 --- .../restdocs/operation/OperationRequestFactory.java | 9 ++++++++- .../restdocs/http/HttpRequestSnippetTests.java | 10 ++++++++++ .../MockMvcRestDocumentationIntegrationTests.java | 2 +- .../preprocess/UriModifyingOperationPreprocessor.java | 10 ++++++---- .../restassured/RestAssuredRequestConverterTests.java | 2 +- .../RestAssuredRestDocumentationIntegrationTests.java | 10 +++++----- .../UriModifyingOperationPreprocessorTests.java | 8 +++++++- 7 files changed, 38 insertions(+), 13 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 961987b5b..198011b45 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -100,10 +100,17 @@ public OperationRequest createFrom(OperationRequest original, private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, byte[] content) { return new HttpHeadersHelper(originalHeaders) - .addIfAbsent(HttpHeaders.HOST, uri.getHost()) + .addIfAbsent(HttpHeaders.HOST, createHostHeader(uri)) .setContentLengthHeader(content).getHeaders(); } + private String createHostHeader(URI uri) { + if (uri.getPort() == -1) { + return uri.getHost(); + } + return uri.getHost() + ":" + uri.getPort(); + } + private HttpHeaders getUpdatedHeaders(HttpHeaders originalHeaders, byte[] updatedContent) { return new HttpHeadersHelper(originalHeaders) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 32e1e6806..df1401ac9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -59,6 +59,16 @@ public void getRequest() throws IOException { .request("http://localhost/foo").header("Alpha", "a").build()); } + @Test + public void getRequestWithPort() throws IOException { + this.snippet.expectHttpRequest("get-request") + .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") + .header(HttpHeaders.HOST, "localhost:8080")); + + new HttpRequestSnippet().document(operationBuilder("get-request") + .request("http://localhost:8080/foo").header("Alpha", "a").build()); + } + @Test public void getRequestWithQueryString() throws IOException { this.snippet.expectHttpRequest("get-request-with-query-string") diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 5d70cfb57..0094abc8a 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -375,7 +375,7 @@ public void preprocessedRequest() throws Exception { .header("Content-Type", "application/json") .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .header("Host", "localhost") + .header("Host", "localhost:8080") .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index 3c7b109d0..5d1566550 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -16,6 +16,7 @@ package org.springframework.restdocs.restassured.operation.preprocess; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -125,8 +126,10 @@ public OperationRequest preprocess(OperationRequest request) { if (this.scheme != null) { uriBuilder.scheme(this.scheme); } + HttpHeaders modifiedHeaders = modify(request.getHeaders()); if (this.host != null) { uriBuilder.host(this.host); + modifiedHeaders.set(HttpHeaders.HOST, this.host); } if (this.port != null) { if (StringUtils.hasText(this.port)) { @@ -136,10 +139,9 @@ public OperationRequest preprocess(OperationRequest request) { uriBuilder.port(null); } } - HttpHeaders modifiedHeaders = modify(request.getHeaders()); - if (this.host != null) { - modifiedHeaders.set(HttpHeaders.HOST, this.host); - } + URI modifiedUri = uriBuilder.build(true).toUri(); + modifiedHeaders.set(HttpHeaders.HOST, modifiedUri.getHost() + + (modifiedUri.getPort() == -1 ? "" : ":" + modifiedUri.getPort())); return this.contentModifyingDelegate.preprocess( new OperationRequestFactory().create(uriBuilder.build(true).toUri(), request.getMethod(), request.getContent(), modifiedHeaders, diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 389a878aa..3bf790982 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -142,7 +142,7 @@ public void headers() { assertThat(request.getHeaders().get("Foo"), is(equalTo(Arrays.asList("bar")))); assertThat(request.getHeaders().get("Accept"), is(equalTo(Arrays.asList("*/*")))); assertThat(request.getHeaders().get("Host"), - is(equalTo(Arrays.asList("localhost")))); + is(equalTo(Arrays.asList("localhost:" + this.port)))); } @Test diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 27c055ae0..f81c91f39 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -268,9 +268,9 @@ public void preprocessedRequest() throws Exception { .filter(document("original-request")) .filter(document("preprocessed-request", preprocessRequest(prettyPrint(), - removeHeaders("a", HttpHeaders.HOST, - HttpHeaders.CONTENT_LENGTH), - replacePattern(pattern, "\"<>\"")))) + replacePattern(pattern, "\"<>\""), + modifyUris().removePort(), + removeHeaders("a", HttpHeaders.CONTENT_LENGTH)))) .get("/").then().statusCode(200); assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), @@ -279,7 +279,7 @@ public void preprocessedRequest() throws Exception { .header("a", "alpha").header("b", "bravo") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .header("Content-Type", "application/json; charset=UTF-8") - .header("Host", "localhost") + .header("Host", "localhost:" + this.port) .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); @@ -291,7 +291,7 @@ public void preprocessedRequest() throws Exception { .header("b", "bravo") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .header("Content-Type", "application/json; charset=UTF-8") - .content(prettyPrinted)))); + .header("Host", "localhost").content(prettyPrinted)))); } @Test diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java index 8ce532886..d8cffc400 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessorTests.java @@ -63,9 +63,11 @@ public void requestUriSchemeCanBeModified() { public void requestUriHostCanBeModified() { this.preprocessor.host("api.example.com"); OperationRequest processed = this.preprocessor - .preprocess(createRequestWithUri("http://api.example.com:12345")); + .preprocess(createRequestWithUri("http://api.foo.com:12345")); assertThat(processed.getUri(), is(equalTo(URI.create("http://api.example.com:12345")))); + assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST), + is(equalTo("api.example.com:12345"))); } @Test @@ -75,6 +77,8 @@ public void requestUriPortCanBeModified() { .preprocess(createRequestWithUri("http://api.example.com:12345")); assertThat(processed.getUri(), is(equalTo(URI.create("http://api.example.com:23456")))); + assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST), + is(equalTo("api.example.com:23456"))); } @Test @@ -83,6 +87,8 @@ public void requestUriPortCanBeRemoved() { OperationRequest processed = this.preprocessor .preprocess(createRequestWithUri("http://api.example.com:12345")); assertThat(processed.getUri(), is(equalTo(URI.create("http://api.example.com")))); + assertThat(processed.getHeaders().getFirst(HttpHeaders.HOST), + is(equalTo("api.example.com"))); } @Test From 37519398c735dc4dca3c5f04bb0b309cd7b1dc19 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jun 2016 11:41:57 +0100 Subject: [PATCH 145/898] Include customized Host header in curl and HTTPie request snippets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the curl and HTTPie request snippets always excluded the Host header, relying on the Host header that’s auto-generated by curl and HTTPie instead. This worked well for the most part, but meant that incorrect snippets were generated when the request was being sent with a custom host header that did not match the header that would be auto-generated. This commit updates CliOperationRequest to only filter out the Host header if it’s the same as the header that would be auto-generated by curl or HTTPie. Closes gh-258 --- .../restdocs/cli/CliOperationRequest.java | 49 ++++++++++++++----- .../restdocs/cli/CurlRequestSnippetTests.java | 14 ++++++ .../cli/HttpieRequestSnippetTests.java | 14 ++++++ 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 97d64c256..ce305278a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -17,8 +17,8 @@ package org.springframework.restdocs.cli; import java.net.URI; +import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -41,20 +41,15 @@ */ final class CliOperationRequest implements OperationRequest { - private static final Set HEADER_FILTERS; - - static { - Set headerFilters = new HashSet<>(); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.HOST)); - headerFilters.add(new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH)); - headerFilters.add(new BasicAuthHeaderFilter()); - HEADER_FILTERS = Collections.unmodifiableSet(headerFilters); - } + private final Set headerFilters; private final OperationRequest delegate; CliOperationRequest(OperationRequest delegate) { this.delegate = delegate; + this.headerFilters = new HashSet<>(Arrays.asList( + new NamedHeaderFilter(HttpHeaders.CONTENT_LENGTH), + new BasicAuthHeaderFilter(), new HostHeaderFilter(delegate.getUri()))); } Parameters getUniqueParameters() { @@ -121,11 +116,20 @@ public HttpHeaders getHeaders() { } private boolean allowedHeader(Map.Entry> header) { - for (HeaderFilter headerFilter : HEADER_FILTERS) { + for (HeaderFilter headerFilter : this.headerFilters) { if (!headerFilter.allow(header.getKey(), header.getValue())) { return false; } } + if (HttpHeaders.HOST.equalsIgnoreCase(header.getKey())) { + if (!header.getValue().isEmpty()) { + String value = header.getValue().get(0); + if (value.equals(this.delegate.getUri().getHost() + ":" + + this.delegate.getUri().getPort())) { + return false; + } + } + } return true; } @@ -187,4 +191,27 @@ public boolean allow(String name, List value) { } + private static final class HostHeaderFilter implements HeaderFilter { + + private final URI uri; + + private HostHeaderFilter(URI uri) { + this.uri = uri; + } + + @Override + public boolean allow(String name, List value) { + if (value.isEmpty() || this.getImplicitHostHeader().equals(value.get(0))) { + return false; + } + return true; + } + + private String getImplicitHostHeader() { + return this.uri.getHost() + + ((this.uri.getPort() == -1) ? "" : ":" + this.uri.getPort()); + } + + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index 86f5a04f7..ba953fed6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -330,4 +330,18 @@ public void customAttributes() throws IOException { .request("http://localhost/foo").build()); } + @Test + public void customHostHeaderIsIncluded() throws IOException { + this.snippet.expectCurlRequest("custom-host-header") + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo' -i" + " -H 'Host: api.example.com'" + + " -H 'Content-Type: application/json' -H 'a: alpha'")); + new CurlRequestSnippet().document( + operationBuilder("custom-host-header").request("http://localhost/foo") + .header(HttpHeaders.HOST, "api.example.com") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 8f9f48dbc..c3b9a398a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -332,4 +332,18 @@ public void customAttributes() throws IOException { .request("http://localhost/foo").build()); } + @Test + public void customHostHeaderIsIncluded() throws IOException { + this.snippet.expectHttpieRequest("custom-host-header") + .withContents(codeBlock("bash").content( + "$ http GET 'http://localhost/foo' 'Host:api.example.com'" + + " 'Content-Type:application/json' 'a:alpha'")); + new HttpieRequestSnippet().document( + operationBuilder("custom-host-header").request("http://localhost/foo") + .header(HttpHeaders.HOST, "api.example.com") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON_VALUE) + .header("a", "alpha").build()); + } + } From 8ca1dfa1ad7e84c4abde73db62a398bb1b9bed48 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jun 2016 17:53:32 +0100 Subject: [PATCH 146/898] Improve handling of requests with both parameters and content Previously, if a MockMvc request was made with parameters and body content, the parameters were omitted from the resulting curl, HTTPie and HTTP request snippets. This commit updates the affected snippets to ensure that the parameters are included. The user-provided content is interpreted as indicating that the parameters should be sent in the query string rather than as form-encoded content. Closes gh-239 --- .../restdocs/cli/CurlRequestSnippet.java | 11 ++++++- .../restdocs/cli/HttpieRequestSnippet.java | 14 ++++++-- .../restdocs/http/HttpRequestSnippet.java | 28 +++++++++++++--- .../restdocs/cli/CurlRequestSnippetTests.java | 21 ++++++++++++ .../cli/HttpieRequestSnippetTests.java | 20 ++++++++++++ .../http/HttpRequestSnippetTests.java | 25 +++++++++++++++ ...kMvcRestDocumentationIntegrationTests.java | 32 +++++++++++++++++++ 7 files changed, 144 insertions(+), 7 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 73eeb72a3..31127c3c4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -69,7 +69,16 @@ protected Map createModel(Operation operation) { } private String getUrl(Operation operation) { - return String.format("'%s'", operation.getRequest().getUri()); + OperationRequest request = operation.getRequest(); + if (!request.getParameters().isEmpty() && includeParametersInUri(request)) { + return String.format("'%s?%s'", request.getUri(), + request.getParameters().toQueryString()); + } + return String.format("'%s'", request.getUri()); + } + + private boolean includeParametersInUri(OperationRequest request) { + return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; } private String getOptions(Operation operation) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 57673b9c3..4c95c04d1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -24,6 +24,7 @@ import java.util.Map.Entry; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -89,7 +90,11 @@ private String getOptions(CliOperationRequest request) { return options.toString(); } - private String getUrl(OperationRequest request) { + private String getUrl(CliOperationRequest request) { + if (!request.getUniqueParameters().isEmpty() && includeParametersInUri(request)) { + return String.format("'%s?%s'", request.getUri(), + request.getParameters().toQueryString()); + } return String.format("'%s'", request.getUri()); } @@ -103,11 +108,16 @@ private String getRequestItems(CliOperationRequest request) { } private void writeOptions(CliOperationRequest request, PrintWriter writer) { - if (!request.getParts().isEmpty() || !request.getUniqueParameters().isEmpty()) { + if (!request.getParts().isEmpty() || (!request.getUniqueParameters().isEmpty() + && !includeParametersInUri(request))) { writer.print("--form "); } } + private boolean includeParametersInUri(CliOperationRequest request) { + return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; + } + private void writeUserOptionIfNecessary(CliOperationRequest request, PrintWriter writer) { String credentials = request.getBasicAuthCredentials(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index cd855f7be..c966f137e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -66,14 +66,33 @@ protected HttpRequestSnippet(Map attributes) { protected Map createModel(Operation operation) { Map model = new HashMap<>(); model.put("method", operation.getRequest().getMethod()); - model.put("path", operation.getRequest().getUri().getRawPath() - + (StringUtils.hasText(operation.getRequest().getUri().getRawQuery()) - ? "?" + operation.getRequest().getUri().getRawQuery() : "")); + model.put("path", getPath(operation.getRequest())); model.put("headers", getHeaders(operation.getRequest())); model.put("requestBody", getRequestBody(operation.getRequest())); return model; } + private String getPath(OperationRequest request) { + String path = request.getUri().getRawPath(); + String queryString = request.getUri().getRawQuery(); + if (!request.getParameters().isEmpty() && includeParametersInUri(request)) { + if (StringUtils.hasText(queryString)) { + queryString = queryString + "&" + request.getParameters().toQueryString(); + } + else { + queryString = request.getParameters().toQueryString(); + } + } + if (StringUtils.hasText(queryString)) { + path = path + "?" + queryString; + } + return path; + } + + private boolean includeParametersInUri(OperationRequest request) { + return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; + } + private List> getHeaders(OperationRequest request) { List> headers = new ArrayList<>(); @@ -172,7 +191,8 @@ private void writeMultipartEnd(PrintWriter writer) { private boolean requiresFormEncodingContentTypeHeader(OperationRequest request) { return request.getHeaders().get(HttpHeaders.CONTENT_TYPE) == null - && isPutOrPost(request) && !request.getParameters().isEmpty(); + && isPutOrPost(request) && (!request.getParameters().isEmpty() + && !includeParametersInUri(request)); } private Map header(String name, String value) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index ba953fed6..e26ca2176 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -61,6 +61,14 @@ public void getRequest() throws IOException { operationBuilder("get-request").request("http://localhost/foo").build()); } + @Test + public void getRequestWithParameter() throws IOException { + this.snippet.expectCurlRequest("get-request").withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo?a=alpha' -i")); + new CurlRequestSnippet().document(operationBuilder("get-request") + .request("http://localhost/foo").param("a", "alpha").build()); + } + @Test public void nonGetRequest() throws IOException { this.snippet.expectCurlRequest("non-get-request").withContents( @@ -344,4 +352,17 @@ public void customHostHeaderIsIncluded() throws IOException { .header("a", "alpha").build()); } + @Test + public void postWithContentAndParameters() throws IOException { + this.snippet.expectCurlRequest("post-with-content-and-parameters") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + + "-X POST -d 'Some content'")); + new CurlRequestSnippet() + .document(operationBuilder("post-with-content-and-parameters") + .request("http://localhost/foo").param("a", "alpha") + .method("POST").param("b", "bravo").content("Some content") + .build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index c3b9a398a..9f8164b39 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -62,6 +62,14 @@ public void getRequest() throws IOException { operationBuilder("get-request").request("http://localhost/foo").build()); } + @Test + public void getRequestWithParameter() throws IOException { + this.snippet.expectHttpieRequest("get-request-with-parameter").withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo?a=alpha'")); + new HttpieRequestSnippet().document(operationBuilder("get-request-with-parameter") + .request("http://localhost/foo").param("a", "alpha").build()); + } + @Test public void nonGetRequest() throws IOException { this.snippet.expectHttpieRequest("non-get-request").withContents( @@ -346,4 +354,16 @@ public void customHostHeaderIsIncluded() throws IOException { .header("a", "alpha").build()); } + @Test + public void postWithContentAndParameters() throws IOException { + this.snippet.expectHttpieRequest("post-with-content-and-parameters").withContents( + codeBlock("bash").content("$ echo 'Some content' | http POST " + + "'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet() + .document(operationBuilder("post-with-content-and-parameters") + .request("http://localhost/foo").method("POST") + .param("a", "alpha").param("b", "bravo").content("Some content") + .build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index df1401ac9..6f7057b37 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -59,6 +59,17 @@ public void getRequest() throws IOException { .request("http://localhost/foo").header("Alpha", "a").build()); } + @Test + public void getRequestWithParameters() throws IOException { + this.snippet.expectHttpRequest("get-request-with-parameters") + .withContents(httpRequest(RequestMethod.GET, "/foo?b=bravo") + .header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); + + new HttpRequestSnippet().document(operationBuilder("get-request-with-parameters") + .request("http://localhost/foo").header("Alpha", "a").param("b", "bravo") + .build()); + } + @Test public void getRequestWithPort() throws IOException { this.snippet.expectHttpRequest("get-request") @@ -103,6 +114,20 @@ public void postRequestWithContent() throws IOException { .request("http://localhost/foo").method("POST").content(content).build()); } + @Test + public void postRequestWithContentAndParameters() throws IOException { + String content = "Hello, world"; + this.snippet.expectHttpRequest("post-request-with-content-and-parameters") + .withContents(httpRequest(RequestMethod.POST, "/foo?a=alpha") + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + + new HttpRequestSnippet() + .document(operationBuilder("post-request-with-content-and-parameters") + .request("http://localhost/foo").method("POST") + .param("a", "alpha").content(content).build()); + } + @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 0094abc8a..2b2df2e8e 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -175,6 +175,22 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { + "-H 'Accept: application/json' -d 'a=alpha'")))); } + @Test + public void curlSnippetWithContentAndParametersOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + mockMvc.perform(post("/").param("a", "alpha").accept(MediaType.APPLICATION_JSON) + .content("some content")).andExpect(status().isOk()) + .andDo(document("curl-snippet-with-content-and-parameters")); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-content-and-parameters/curl-request.adoc"), + is(snippet(asciidoctor()) + .withContents(codeBlock(asciidoctor(), "bash").content("$ curl " + + "'http://localhost:8080/?a=alpha' -i -X POST " + + "-H 'Accept: application/json' -d 'some content'")))); + } + @Test public void httpieSnippetWithContent() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -207,6 +223,22 @@ public void httpieSnippetWithQueryStringOnPost() throws Exception { + "'Accept:application/json' 'a=alpha'")))); } + @Test + public void httpieSnippetWithContentAndParametersOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + mockMvc.perform(post("/").param("a", "alpha").content("some content") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("httpie-snippet-post-with-content-and-parameters")); + assertThat( + new File( + "build/generated-snippets/httpie-snippet-post-with-content-and-parameters/httpie-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ echo " + "'some content' | http POST " + + "'http://localhost:8080/?a=alpha' " + + "'Accept:application/json'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) From c6f20f213860bb09c06fd57fe7ad9541080b2b2d Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 24 Jun 2016 10:07:42 +0900 Subject: [PATCH 147/898] Correct javadoc in LinksSnippet Closes gh-271 --- .../springframework/restdocs/hypermedia/LinksSnippet.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java index f0a4cb30e..62543d5b8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/LinksSnippet.java @@ -243,8 +243,8 @@ protected Map createModelForDescriptor(LinkDescriptor descriptor } /** - * Returns a new {@code RequestHeadersSnippet} configured with this snippet's link - * extractor and attributes, and its descriptors combined with the given + * Returns a new {@code LinksSnippet} configured with this snippet's link extractor + * and attributes, and its descriptors combined with the given * {@code additionalDescriptors}. * @param additionalDescriptors the additional descriptors * @return the new snippet @@ -254,8 +254,8 @@ public final LinksSnippet and(LinkDescriptor... additionalDescriptors) { } /** - * Returns a new {@code RequestHeadersSnippet} configured with this snippet's link - * extractor and attributes, and its descriptors combined with the given + * Returns a new {@code LinksSnippet} configured with this snippet's link extractor + * and attributes, and its descriptors combined with the given * {@code additionalDescriptors}. * @param additionalDescriptors the additional descriptors * @return the new snippet From a1d5d75c4e4caf16bae5f46c7249b18bd2a7e0db Mon Sep 17 00:00:00 2001 From: Jennifer Strater Date: Mon, 20 Jun 2016 21:29:05 -0500 Subject: [PATCH 148/898] Split samples table into three tables grouped by type Closes gh-266 --- docs/src/docs/asciidoc/getting-started.adoc | 45 +++++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 2a1d161cb..853e2a473 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -10,23 +10,12 @@ This section describes how to get started with Spring REST Docs. If you want to jump straight in, a number of sample applications are available: + [cols="3,2,10"] +.MockMvc |=== | Sample | Build system | Description -| {samples}/rest-assured[REST Assured] -| Gradle -| Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. - -| {samples}/rest-notes-grails[Grails] -| Gradle -| Demonstrates the use of Spring REST docs with https://grails.org[Grails] and https://github.com/spockframework/spock[Spock] - -| {samples}/rest-notes-slate[Slate] -| Gradle -| Demonstrates the use of Spring REST Docs with Markdown and - http://github.com/tripit/slate[Slate]. - | {samples}/rest-notes-spring-data-rest[Spring Data REST] | Maven | Demonstrates the creation of a getting started guide and an API guide for a service @@ -37,6 +26,36 @@ If you want to jump straight in, a number of sample applications are available: | Demonstrates the creation of a getting started guide and an API guide for a service implemented using http://projects.spring.io/spring-hateoas/[Spring HATEOAS]. +|=== + + +[cols="3,2,10"] +.REST Assured +|=== +| Sample | Build system | Description + +| {samples}/rest-notes-grails[Grails] +| Gradle +| Demonstrates the use of Spring REST docs with https://grails.org[Grails] and + https://github.com/spockframework/spock[Spock]. + +| {samples}/rest-assured[REST Assured] +| Gradle +| Demonstrates the use of Spring REST Docs with http://rest-assured.io[REST Assured]. + +|=== + + +[cols="3,2,10"] +.Advanced +|=== +| Sample | Build system | Description + +| {samples}/rest-notes-slate[Slate] +| Gradle +| Demonstrates the use of Spring REST Docs with Markdown and + http://github.com/tripit/slate[Slate]. + | {samples}/testng[TestNG] | Gradle | Demonstrates the use of Spring REST Docs with http://testng.org[TestNG]. From 42613bf3f9e322515530b1c082362f07b4cb4a9f Mon Sep 17 00:00:00 2001 From: Jennifer Strater Date: Mon, 20 Jun 2016 18:38:50 -0500 Subject: [PATCH 149/898] Add README to Grails sample Closes gh-254 Closes gh-268 --- samples/rest-notes-grails/README.md | 95 ++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/samples/rest-notes-grails/README.md b/samples/rest-notes-grails/README.md index 386292af4..d6c1be30c 100644 --- a/samples/rest-notes-grails/README.md +++ b/samples/rest-notes-grails/README.md @@ -1,2 +1,93 @@ -# grails-spring-restdocs-example -Example of using Spring REST Docs with Grails +# rest-notes-grails + +## Overview + +This is a sample project using Grails 3, Spock, and Spring REST docs. For more +information about the Grails framework please see [grails.org](http://grails.org). + +Grails is built on top of Spring Boot and Gradle so there are a few different ways to +run this project including: + +### Gradle Wrapper (recommended) + +The gradle wrapper allows a project to build without having Gradle installed locally. The +executable file will acquire the version of Gradle and other dependencies recommended for +this project. This is especially important since some versions of Gradle may cause +conflicts with this project. + +On Unix-like platforms, such as Linux and Mac OS X: + +``` +$ ./gradlew run +``` + +On Windows: + +``` +$ gradlew run +``` + +*Please note*, if you are including integration tests in Grails, they will not run as +part of the `gradle test` task. Run them via the build task or individually as +`gradle integrationTest` + +### Gradle Command Line + +Clean the project: + +``` +$ gradle clean +``` + +Build the project: + +``` +$ gradle build +``` + +Run the project: + +``` +$ gradle run +``` + +### Grails Command Line + +Grails applications also have a command line feature useful for code generation and +running projects locally. The command line is accessible by typing `grails` in the +terminal at the root of the project. Please ensure you are running the correct version +of Grails as specified in [gradle.properties](gradle.properties) + +Similar to `gradle clean`, this task destroys the `build` directory and cached assets. + +``` +grails> clean +``` + +The 'test-app' task runs all of the tests for the project. + +``` +grails> test-app +``` + +The `run-app` task is used to run the application locally. By default, the project is +run in development mode including automatic reloading and not caching static assets. It +is not suggested to use this in production. + +``` +grails> run-app +``` + +### Building and Viewing the Docs + +This is an example of the Grails API profile. Therefore, there is no view layer to +return the docs as static assets. The result of running `asciidoctor` or `build` is that +the docs are sent to `/build/asciidoc/`. You can then publish them to a destination of +your choosing using the [gradle github-pages](https://github.com/ajoberstar/gradle-git) +plugin or similar. + +To just generate documentation and not run the application use: + +``` +$ ./gradlew asciidoctor +``` From 417547e61a848c64623ee58bb49900f9ba1b2ece Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 28 Jun 2016 11:17:37 +0100 Subject: [PATCH 150/898] Support more types of content in RestAssuredRequestConverter Previously, RestAssuredRequestConverter only supported request and request part content that was null, a String or a byte[]. This was particularly problematic for multipart requests as RestAssured performs some internal conversion which meant that, no matter what the when a byte[] was provided it was wrapped in a ByteArrayInputStream meaning that the converter could not read it. The commit improves RestAssuredRequestConverter so that it will now convert File content and InputStream content where the stream supported reset(). Closes gh-252 --- .../RestAssuredRequestConverter.java | 38 ++++++++ .../RestAssuredRequestConverterTests.java | 94 +++++++++++++++++-- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index d2e60f0ce..1673366a0 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -16,6 +16,9 @@ package org.springframework.restdocs.restassured; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -35,6 +38,8 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StreamUtils; /** * A converter for creating an {@link OperationRequest} from a REST Assured @@ -64,6 +69,12 @@ private byte[] convertContent(Object content) { else if (content instanceof byte[]) { return (byte[]) content; } + else if (content instanceof File) { + return copyToByteArray((File) content); + } + else if (content instanceof InputStream) { + return copyToByteArray((InputStream) content); + } else if (content == null) { return new byte[0]; } @@ -73,6 +84,33 @@ else if (content == null) { } } + private byte[] copyToByteArray(File file) { + try { + return FileCopyUtils.copyToByteArray(file); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to read content from file " + file, + ex); + } + } + + private byte[] copyToByteArray(InputStream inputStream) { + try { + inputStream.reset(); + } + catch (IOException ex) { + throw new IllegalStateException("Cannot read content from input stream " + + inputStream + " due to reset() failure"); + } + try { + return StreamUtils.copyToByteArray(inputStream); + } + catch (IOException ex) { + throw new IllegalStateException( + "Failed to read content from input stream " + inputStream, ex); + } + } + private HttpHeaders extractHeaders(FilterableRequestSpecification requestSpec) { HttpHeaders httpHeaders = new HttpHeaders(); for (Header header : requestSpec.getHeaders()) { diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 3bf790982..9c4f7615e 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -18,6 +18,8 @@ import java.io.ByteArrayInputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.net.URI; import java.util.Arrays; import java.util.Collection; @@ -198,25 +200,105 @@ public void objectBody() { } @Test - public void inputStreamBodyIsNotSupported() { + public void byteArrayInputStreamBody() { RequestSpecification requestSpec = RestAssured.given() .body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) .port(this.port); requestSpec.post(); - this.thrown.expectMessage( - equalTo("Unsupported request content: java.io.ByteArrayInputStream")); - this.factory.convert((FilterableRequestSpecification) requestSpec); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContent(), is(equalTo(new byte[] { 1, 2, 3, 4 }))); } @Test - public void fileBodyIsNotSupported() { + public void fileBody() { RequestSpecification requestSpec = RestAssured.given() .body(new File("src/test/resources/body.txt")).port(this.port); requestSpec.post(); - this.thrown.expectMessage(equalTo("Unsupported request content: java.io.File")); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getContentAsString(), is(equalTo("file"))); + } + + @Test + public void fileInputStreamBody() throws FileNotFoundException { + FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); + RequestSpecification requestSpec = RestAssured.given().body(inputStream) + .port(this.port); + requestSpec.post(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Cannot read content from input stream " + inputStream + + " due to reset() failure"); this.factory.convert((FilterableRequestSpecification) requestSpec); } + @Test + public void multipartWithByteArrayInputStreamBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("foo", "foo.txt", new ByteArrayInputStream("foo".getBytes())); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("foo"))); + } + + @Test + public void multipartWithStringBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("control", "foo"); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("foo"))); + } + + @Test + public void multipartWithByteArrayBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("control", "file", "foo".getBytes()); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("foo"))); + } + + @Test + public void multipartWithFileBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart(new File("src/test/resources/body.txt")); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("file"))); + } + + @Test + public void multipartWithFileInputStreamBody() throws FileNotFoundException { + FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("foo", "foo.txt", inputStream); + requestSpec.post(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage("Cannot read content from input stream " + inputStream + + " due to reset() failure"); + this.factory.convert((FilterableRequestSpecification) requestSpec); + } + + @Test + public void multipartWithObjectBody() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .multiPart("control", new ObjectBody("bar")); + requestSpec.post(); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getParts().iterator().next().getContentAsString(), + is(equalTo("{\"foo\":\"bar\"}"))); + } + /** * Minimal test web application. */ From 0a0136ca5867813cc74984fbc7a77c7ab04cb0cb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 5 Jul 2016 12:49:17 +0100 Subject: [PATCH 151/898] Move spring-webmvc dependency to where it's needed Previously, spring-restdocs-core had a dependency on spring-webmvc however it only used classes from spring-web. This commit moves the spring-webmvc dependency from spring-restdocs-core to spring-restdocs-mockmvc where it's needed. It's now in the runtime scope (rather than compile) as it's not needed at compile time but is needed at runtime by the MockMvc support in spring-test. Closes gh-273 --- spring-restdocs-core/build.gradle | 2 +- spring-restdocs-mockmvc/build.gradle | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 1bee13f54..4a8417072 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -26,7 +26,7 @@ task jmustacheRepackJar(type: Jar) { repackJar -> dependencies { compile 'com.fasterxml.jackson.core:jackson-databind' - compile 'org.springframework:spring-webmvc' + compile 'org.springframework:spring-web' compile 'javax.servlet:javax.servlet-api' compile files(jmustacheRepackJar) jarjar 'com.googlecode.jarjar:jarjar:1.3' diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index b93fba78d..d8a0529cb 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -4,6 +4,7 @@ dependencies { compile 'org.springframework:spring-test' compile project(':spring-restdocs-core') optional 'junit:junit' + runtime 'org.springframework:spring-webmvc' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' From a3159b6f1c851044e1c55ec6888c6e880a23b5f4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 5 Jul 2016 15:37:22 +0100 Subject: [PATCH 152/898] Update README to link to restdocsext-jersey third-party extension --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 50f3ebe86..c9b178feb 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ There are several that you can contribute to Spring REST Docs: | Name | Description | | ---- | ----------- | | [restdocs-wiremock][17] | Auto-generate [WireMock][18] stubs as part of documenting your RESTful API | +| [restdocsext-jersey][19] | Enables Spring REST Docs to be used with [Jersey's test framework][20] | ## Licence @@ -60,4 +61,6 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [15]: http://stackoverflow.com/tags/spring-restdocs [16]: https://gitter.im/spring-projects/spring-restdocs [17]: https://github.com/ePages-de/restdocs-wiremock -[18]: http://wiremock.org/ \ No newline at end of file +[18]: http://wiremock.org/ +[19]: https://github.com/RESTDocsEXT/restdocsext-jersey +[20]: https://jersey.java.net/documentation/latest/test-framework.html \ No newline at end of file From 4f00cebaae10dfc9f228090e1cafc8ce35e16eeb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 11 Jul 2016 10:47:14 +0100 Subject: [PATCH 153/898] Adopt the new CLA signing process --- CONTRIBUTING.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b609d53c..4d7046ebc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,8 +15,7 @@ spring-code-of-conduct@pivotal.io. Before we accept a non-trivial patch or pull request we will need you to sign the [contributor's license agreement (CLA)][2]. Signing the contributor's agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept -your contributions, and you will get an author credit if we do. Please use "Andy -Wilkinson" in the project lead field when you complete the form. +your contributions, and you will get an author credit if we do. ## Code conventions and housekeeping @@ -61,5 +60,5 @@ The project can then be imported into Eclipse using `File -> Import…` and then `General -> Existing Projects into Workspace`. [1]: CODE_OF_CONDUCT.md -[2]: https://support.springsource.com/spring_committer_signup +[2]: https://cla.pivotal.io/sign/spring [3]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html \ No newline at end of file From 19d7337e264cdfc6e526d37dac81617194fc22c6 Mon Sep 17 00:00:00 2001 From: Johnny Lim Date: Fri, 22 Jul 2016 08:12:04 +0900 Subject: [PATCH 154/898] Remove unnecessary Host header set in UriModifyingOperationPreprocessor Closes gh-280 --- .../preprocess/UriModifyingOperationPreprocessor.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index 5d1566550..1a4798147 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -126,10 +126,8 @@ public OperationRequest preprocess(OperationRequest request) { if (this.scheme != null) { uriBuilder.scheme(this.scheme); } - HttpHeaders modifiedHeaders = modify(request.getHeaders()); if (this.host != null) { uriBuilder.host(this.host); - modifiedHeaders.set(HttpHeaders.HOST, this.host); } if (this.port != null) { if (StringUtils.hasText(this.port)) { @@ -140,6 +138,7 @@ public OperationRequest preprocess(OperationRequest request) { } } URI modifiedUri = uriBuilder.build(true).toUri(); + HttpHeaders modifiedHeaders = modify(request.getHeaders()); modifiedHeaders.set(HttpHeaders.HOST, modifiedUri.getHost() + (modifiedUri.getPort() == -1 ? "" : ":" + modifiedUri.getPort())); return this.contentModifyingDelegate.preprocess( From eb46eaa3c55b5b6099aed9c5e406da602ee2f386 Mon Sep 17 00:00:00 2001 From: mle Date: Thu, 21 Jul 2016 20:06:15 +0200 Subject: [PATCH 155/898] Improve documention on how to use .adoc snippets Closes gh-279 --- docs/src/docs/asciidoc/getting-started.adoc | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 853e2a473..9bd7907a4 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -419,7 +419,26 @@ that can be produced by Spring REST Docs. [[getting-started-using-the-snippets]] === Using the snippets -The generated snippets can be included in your documentation using the +Before using the generated snippets from above, an ```\*.adoc``` source file must be manually created. +Its filename does not matter for either of the two build tools' Asciidoctor plugins mentioned +above and will also be the name of the finally generated ```*.html``` file. +For the exact locations refer to the following table. + +[cols="2,5,8"] +|=== +| Build tool | Source files | Generated files + +| Maven +| ```src/main/asciidoc/*.adoc``` +| ```${project.build.directory}/generated-docs/*.html``` + +| Gradle +| ```src/docs/asciidoc/*.adoc``` +| ```$buildDir/asciidoc/*.html``` + +|=== + +The generated snippets can then be included in the manually created Asciidoctor file from above using the http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. The `snippets` attribute specified in the <> can be used to reference the snippets output directory. For example: From 9c97c8b9bf17a8ebfa50ca38450e356da6663a92 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 10:45:14 +0100 Subject: [PATCH 156/898] Polish "Improve documention on how to use .adoc snippets" See gh-279 --- docs/src/docs/asciidoc/getting-started.adoc | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 9bd7907a4..0dd076206 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -419,26 +419,27 @@ that can be produced by Spring REST Docs. [[getting-started-using-the-snippets]] === Using the snippets -Before using the generated snippets from above, an ```\*.adoc``` source file must be manually created. -Its filename does not matter for either of the two build tools' Asciidoctor plugins mentioned -above and will also be the name of the finally generated ```*.html``` file. -For the exact locations refer to the following table. +Before using the generated snippets, a `.adoc` source file must be created. You can name +the file whatever you like as long as it has a `.adoc` suffix. The result HTML file will +have the same name but with a `.html` suffix. The default location of the source files and +the resulting HTML files depends on whether you are using Maven or Gradle: [cols="2,5,8"] |=== | Build tool | Source files | Generated files | Maven -| ```src/main/asciidoc/*.adoc``` -| ```${project.build.directory}/generated-docs/*.html``` +| `src/main/asciidoc/*.adoc` +| `target/generated-docs/*.html` | Gradle -| ```src/docs/asciidoc/*.adoc``` -| ```$buildDir/asciidoc/*.html``` +| `src/docs/asciidoc/*.adoc` +| `build/asciidoc/html5/*.html` |=== -The generated snippets can then be included in the manually created Asciidoctor file from above using the +The generated snippets can then be included in the manually created Asciidoctor file from +above using the http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. The `snippets` attribute specified in the <> can be used to reference the snippets output directory. For example: From 86c2310b90d0763b2d0dc93a01083d05fc474372 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 11:44:43 +0100 Subject: [PATCH 157/898] Improve message when fields cannot be documented due to empty body Closes gh-278 --- .../restdocs/payload/AbstractFieldsSnippet.java | 13 +++++++++++-- .../payload/RequestFieldsSnippetFailureTests.java | 11 +++++++++++ .../payload/ResponseFieldsSnippetFailureTests.java | 11 +++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index fcc67c360..393dd8fd8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -43,6 +43,8 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private final boolean ignoreUndocumentedFields; + private final String type; + /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields}. The fields will be documented using the given @@ -88,6 +90,7 @@ protected AbstractFieldsSnippet(String type, List descriptors, } this.fieldDescriptors = descriptors; this.ignoreUndocumentedFields = ignoreUndocumentedFields; + this.type = type; } @Override @@ -116,13 +119,19 @@ protected Map createModel(Operation operation) { private ContentHandler getContentHandler(Operation operation) { MediaType contentType = getContentType(operation); ContentHandler contentHandler; + try { + byte[] content = getContent(operation); + if (content.length == 0) { + throw new SnippetException("Cannot document " + this.type + + " fields as the " + this.type + " body is empty"); + } if (contentType != null && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - contentHandler = new XmlContentHandler(getContent(operation)); + contentHandler = new XmlContentHandler(content); } else { - contentHandler = new JsonContentHandler(getContent(operation)); + contentHandler = new JsonContentHandler(content); } } catch (IOException ex) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index eeab98cfa..a6dceaf0f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -99,6 +99,17 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException .content("{ \"a\": { \"c\": 5 }}").build()); } + @Test + public void attemptToDocumentFieldsWithNoRequestBody() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + equalTo("Cannot document request fields as the request body is empty")); + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("no-request-body", + this.snippet.getOutputDirectory()).request("http://localhost") + .build()); + } + @Test public void undocumentedXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index 98a31d45a..e3385a03a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -50,6 +50,17 @@ public class ResponseFieldsSnippetFailureTests { @Rule public ExpectedException thrown = ExpectedException.none(); + @Test + public void attemptToDocumentFieldsWithNoResponseBody() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + equalTo("Cannot document response fields as the response body is empty")); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) + .document(new OperationBuilder("no-response-body", + this.snippet.getOutputDirectory()).request("http://localhost") + .build()); + } + @Test public void undocumentedXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); From 78318775e0a24ee901e959531fd0fbeb6a083b4b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 13:20:44 +0100 Subject: [PATCH 158/898] Cause a failure when field's documented and actual types do not match Closes gh-276 --- .../payload/AbstractFieldsSnippet.java | 11 +++++- .../restdocs/payload/ContentHandler.java | 8 ++-- .../FieldTypesDoNotMatchException.java | 39 +++++++++++++++++++ .../restdocs/payload/JsonContentHandler.java | 25 +++++++++--- .../restdocs/payload/XmlContentHandler.java | 15 ++++--- .../RequestFieldsSnippetFailureTests.java | 27 +++++++++++++ .../payload/RequestFieldsSnippetTests.java | 28 +++++++++++++ .../ResponseFieldsSnippetFailureTests.java | 28 ++++++++++++- .../payload/ResponseFieldsSnippetTests.java | 27 +++++++++++++ 9 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 393dd8fd8..a21bf524c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -100,8 +100,15 @@ protected Map createModel(Operation operation) { validateFieldDocumentation(contentHandler); for (FieldDescriptor descriptor : this.fieldDescriptors) { - if (descriptor.getType() == null) { - descriptor.type(contentHandler.determineFieldType(descriptor.getPath())); + try { + descriptor.type(contentHandler.determineFieldType(descriptor)); + } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + + descriptor.getPath() + "' as it is not present in the " + + "payload. Please provide a type using " + + "FieldDescriptor.type(Object type)."; + throw new FieldTypeRequiredException(message); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java index ce99ff00f..45865f4ba 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ContentHandler.java @@ -50,12 +50,12 @@ interface ContentHandler { String getUndocumentedContent(List fieldDescriptors); /** - * Returns the type of the field with the given {@code path} based on the content of - * the payload. + * Returns the type of the field that is described by the given + * {@code fieldDescriptor} based on the content of the payload. * - * @param path the field path + * @param fieldDescriptor the field descriptor * @return the type of the field */ - Object determineFieldType(String path); + Object determineFieldType(FieldDescriptor fieldDescriptor); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java new file mode 100644 index 000000000..cfb8667a9 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldTypesDoNotMatchException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014-2016 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; + +/** + * A {@code FieldTypesDoNotMatchException} is thrown when the documented and actual types + * of a field do not match. + * + * @author Andy Wilkinson + */ +class FieldTypesDoNotMatchException extends RuntimeException { + + /** + * Creates a new {@code FieldTypesDoNotMatchException} for the field described by the + * given {@code fieldDescriptor} that has the given {@code actualType}. + * + * @param fieldDescriptor the field + * @param actualType the actual type of the field + */ + FieldTypesDoNotMatchException(FieldDescriptor fieldDescriptor, Object actualType) { + super("The documented type of the field '" + fieldDescriptor.getPath() + "' is " + + fieldDescriptor.getType() + " but the actual type is " + actualType); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index 779b75fbc..f1fff9ce4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -34,6 +34,8 @@ class JsonContentHandler implements ContentHandler { private final JsonFieldProcessor fieldProcessor = new JsonFieldProcessor(); + private final JsonFieldTypeResolver fieldTypeResolver = new JsonFieldTypeResolver(); + private final ObjectMapper objectMapper = new ObjectMapper() .enable(SerializationFeature.INDENT_OUTPUT); @@ -93,15 +95,26 @@ private boolean isEmpty(Object object) { } @Override - public Object determineFieldType(String path) { + public Object determineFieldType(FieldDescriptor fieldDescriptor) { + if (fieldDescriptor.getType() == null) { + return this.fieldTypeResolver.resolveFieldType(fieldDescriptor.getPath(), + readContent()); + } + if (!(fieldDescriptor.getType() instanceof JsonFieldType)) { + return fieldDescriptor.getType(); + } + JsonFieldType descriptorFieldType = (JsonFieldType) fieldDescriptor.getType(); try { - return new JsonFieldTypeResolver().resolveFieldType(path, readContent()); + JsonFieldType actualFieldType = this.fieldTypeResolver + .resolveFieldType(fieldDescriptor.getPath(), readContent()); + if (descriptorFieldType == JsonFieldType.VARIES + || descriptorFieldType == actualFieldType) { + return descriptorFieldType; + } + throw new FieldTypesDoNotMatchException(fieldDescriptor, actualFieldType); } catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" + path + "' as" - + " it is not present in the payload. Please provide a type using" - + " FieldDescriptor.type(Object type)."; - throw new FieldTypeRequiredException(message); + return fieldDescriptor.getType(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 67e2763d5..2e8f99e74 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -153,15 +153,14 @@ private String prettyPrint(Document document) { } @Override - public Object determineFieldType(String path) { - try { - return new JsonFieldTypeResolver().resolveFieldType(path, readPayload()); + public Object determineFieldType(FieldDescriptor fieldDescriptor) { + if (fieldDescriptor.getType() != null) { + return fieldDescriptor.getType(); } - catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" + path + "' as" - + " it is not present in the payload. Please provide a type using" - + " FieldDescriptor.type(Object type)."; - throw new FieldTypeRequiredException(message); + else { + throw new FieldTypeRequiredException("The type of a field in an XML payload " + + "cannot be determined automatically. Please provide a type using " + + "FieldDescriptor.type(Object type)"); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index a6dceaf0f..f27633c2f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -110,6 +110,33 @@ public void attemptToDocumentFieldsWithNoRequestBody() throws IOException { .build()); } + @Test + public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException { + this.thrown.expect(FieldTypesDoNotMatchException.class); + this.thrown.expectMessage(equalTo("The documented type of the field 'a' is" + + " Object but the actual type is Number")); + new RequestFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) + .document(new OperationBuilder("mismatched-field-types", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("{ \"a\": 5 }").build()); + } + + @Test + public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException { + this.thrown.expect(FieldTypesDoNotMatchException.class); + this.thrown.expectMessage(equalTo("The documented type of the field '[].a' is" + + " Object but the actual type is Varies")); + new RequestFieldsSnippet(Arrays.asList( + fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) + .document(new OperationBuilder("mismatched-field-types", + this.snippet.getOutputDirectory()) + .request("http://localhost") + .content("[{ \"a\": 5 },{ \"a\": \"b\" }]") + .build()); + } + @Test public void undocumentedXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index fa23f3467..32e34be9e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -177,6 +177,34 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { .build()); } + @Test + public void fieldWithExplictExactlyMatchingType() throws IOException { + this.snippet + .expectRequestFields("request-field-with-explicit-exactly-matching-type") + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Number`", "one")); + + new RequestFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) + .document(operationBuilder( + "request-field-with-explicit-exactly-matching-type") + .request("http://localhost") + .content("{\"a\": 5 }").build()); + } + + @Test + public void fieldWithExplictVariesType() throws IOException { + this.snippet.expectRequestFields("request-field-with-explicit-varies-type") + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Varies`", "one")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type(JsonFieldType.VARIES))).document( + operationBuilder("request-field-with-explicit-varies-type") + .request("http://localhost").content("{\"a\": 5 }") + .build()); + } + @Test public void xmlRequestFields() throws IOException { this.snippet.expectRequestFields("xml-request") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index e3385a03a..bfe50ad0c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -57,8 +57,32 @@ public void attemptToDocumentFieldsWithNoResponseBody() throws IOException { equalTo("Cannot document response fields as the response body is empty")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) .document(new OperationBuilder("no-response-body", - this.snippet.getOutputDirectory()).request("http://localhost") - .build()); + this.snippet.getOutputDirectory()).build()); + } + + @Test + public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException { + this.thrown.expect(FieldTypesDoNotMatchException.class); + this.thrown.expectMessage(equalTo("The documented type of the field 'a' is" + + " Object but the actual type is Number")); + new ResponseFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) + .document(new OperationBuilder("mismatched-field-types", + this.snippet.getOutputDirectory()).response() + .content("{ \"a\": 5 }}").build()); + } + + @Test + public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException { + this.thrown.expect(FieldTypesDoNotMatchException.class); + this.thrown.expectMessage(equalTo("The documented type of the field '[].a' is" + + " Object but the actual type is Varies")); + new ResponseFieldsSnippet(Arrays.asList( + fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) + .document(new OperationBuilder("mismatched-field-types", + this.snippet.getOutputDirectory()).response() + .content("[{ \"a\": 5 },{ \"a\": \"b\" }]") + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index db13d9424..e0525888d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -185,6 +185,33 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { .build()); } + @Test + public void fieldWithExplictExactlyMatchingType() throws IOException { + this.snippet + .expectResponseFields( + "response-field-with-explicit-exactly-matching-type") + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Number`", "one")); + + new ResponseFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) + .document(operationBuilder( + "response-field-with-explicit-exactly-matching-type") + .response().content("{\"a\": 5 }").build()); + } + + @Test + public void fieldWithExplictVariesType() throws IOException { + this.snippet.expectResponseFields("response-field-with-explicit-varies-type") + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Varies`", "one")); + + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") + .type(JsonFieldType.VARIES))).document( + operationBuilder("response-field-with-explicit-varies-type") + .response().content("{\"a\": 5 }").build()); + } + @Test public void xmlResponseFields() throws IOException { this.snippet.expectResponseFields("xml-response") From 3dceea8bb5ce8454ce86a0c14cdc0332ee64270f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 14:19:13 +0100 Subject: [PATCH 159/898] Document the tableCellContent Mustache lambda Closes gh-251 --- .../asciidoc/working-with-asciidoctor.adoc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index ce166410b..b3981dfeb 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -69,6 +69,24 @@ The title of a table can be specified using a line prefixed by a `.`: +[[working-with-asciidoctor-customizing-tables-title]] +==== Avoiding table formatting problems + +Asciidoctor uses the `|` character to delimit cells in a table. This can cause problems +if you want a `|` to appear in a cell's contents. The problem can be avoided by +escaping the `|` with a backslash, i.e. by using `\|` rather than `|`. + +All of the default Asciidoctor snippet templates perform this escaping automatically +use a Mustache lamba named `tableCellContent`. If you write your own custom templates +you may want to use this lamba. For example, to escape `|` characters +in a cell that contains the value of a `description` attribute: + +---- +| {{#tableCellContent}}{{description}}{{/tableCellContent}} +---- + + + ==== Further reading Refer to the http://asciidoctor.org/docs/user-manual/#tables[Tables section of From 3ec945fe58845893847fd1e931f00ab0279f36b8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 14:30:09 +0100 Subject: [PATCH 160/898] Make it clearer how to use RestDocumentationResultHandler.document Closes gh-255 --- .../mockmvc/RestDocumentationResultHandler.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java index 5a85fc5c2..1522fa2d0 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/RestDocumentationResultHandler.java @@ -24,6 +24,7 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.Snippet; import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.util.Assert; @@ -60,7 +61,8 @@ public void handle(MvcResult result) throws Exception { * * @param snippets the snippets to add * @return this {@code RestDocumentationResultHandler} - * @deprecated since 1.1 in favor of {@link #document(Snippet...)} + * @deprecated since 1.1 in favor of {@link #document(Snippet...)} and passing the + * return value into {@link ResultActions#andDo(ResultHandler)} */ @Deprecated public RestDocumentationResultHandler snippets(Snippet... snippets) { @@ -69,8 +71,17 @@ public RestDocumentationResultHandler snippets(Snippet... snippets) { } /** - * Creates a new {@link RestDocumentationResultHandler} that will produce - * documentation using the given {@code snippets}. + * Creates a new {@link RestDocumentationResultHandler} to be passed into + * {@link ResultActions#andDo(ResultHandler)} that will produce documentation using + * the given {@code snippets}. For example: + * + *
          +	 * this.mockMvc.perform(MockMvcRequestBuilders.get("/search"))
          +	 *     .andExpect(status().isOk())
          +	 *     .andDo(this.documentationHandler.document(responseFields(
          +	 *          fieldWithPath("page").description("The requested Page")
          +	 *     ));
          +	 * 
          * * @param snippets the snippets * @return the new result handler From 6c6054178ea1ee00b42933b5cf87b9eca1f600d7 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 22 Jul 2016 14:41:45 +0100 Subject: [PATCH 161/898] Upgrade tests and samples to use Spring Boot 1.3.6.RELEASE --- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- spring-restdocs-restassured/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index fa089b3ec..a02386b96 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' } } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index ae6b3f644..e7cd576b8 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' } } diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index eb5b467c3..8c17bc420 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 1.3.5.RELEASE + 1.3.6.RELEASE diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index fdde83881..28211f126 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' } } diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 05c7eea82..b962681ec 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.5.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' } } diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index ed8fca51a..a861c7e51 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -12,7 +12,7 @@ dependencies { testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.5.RELEASE' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.6.RELEASE' testCompile 'org.springframework:spring-test' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') testRuntime 'commons-logging:commons-logging:1.2' From 8a91f170545bcf4eb6a9745d50ad0a8d1e45239e Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Mon, 25 Jul 2016 09:24:28 +0000 Subject: [PATCH 162/898] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 179a34021..af18773ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.1.BUILD-SNAPSHOT +version=1.1.2.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From d9192380230461573e3715e6ff60104af21d62e4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 11:06:49 +0100 Subject: [PATCH 163/898] Update the samples following the release of 1.1.1.RELEASE --- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-grails/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index a02386b96..6508143d2 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 3d425282b..74a25c404 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -31,7 +31,7 @@ apply plugin: "org.grails.grails-web" ext { grailsVersion = project.grailsVersion gradleWrapperVersion = project.gradleWrapperVersion - restDocsVersion = "1.1.1.BUILD-SNAPSHOT" + restDocsVersion = "1.1.2.BUILD-SNAPSHOT" snippetsDir = file('src/docs/generated-snippets') } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index e7cd576b8..d0a057657 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -26,7 +26,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 8c17bc420..d01f29d78 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.1.1.BUILD-SNAPSHOT + 1.1.2.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 28211f126..b1a9aa41f 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index b962681ec..723b2430a 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -29,7 +29,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.1.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' From 2db7ab5e711d7bd8afe37f0bf8aeb8b30809da1f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 11:08:36 +0100 Subject: [PATCH 164/898] Start work on 1.2.0 --- gradle.properties | 2 +- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-grails/build.gradle | 2 +- samples/rest-notes-slate/build.gradle | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 2 +- samples/rest-notes-spring-hateoas/build.gradle | 2 +- samples/testng/build.gradle | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index af18773ba..0f5d13f0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.2.BUILD-SNAPSHOT +version=1.2.0.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 6508143d2..4d5bb5237 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 74a25c404..535ba3150 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -31,7 +31,7 @@ apply plugin: "org.grails.grails-web" ext { grailsVersion = project.grailsVersion gradleWrapperVersion = project.gradleWrapperVersion - restDocsVersion = "1.1.2.BUILD-SNAPSHOT" + restDocsVersion = "1.2.0.BUILD-SNAPSHOT" snippetsDir = file('src/docs/generated-snippets') } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index d0a057657..91aea3b80 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -26,7 +26,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index d01f29d78..21699808e 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,7 +18,7 @@ UTF-8 1.7 - 1.1.2.BUILD-SNAPSHOT + 1.2.0.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index b1a9aa41f..1be4c82f1 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -30,7 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 723b2430a..f1e791c84 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -29,7 +29,7 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') } -ext['spring-restdocs.version'] = '1.1.2.BUILD-SNAPSHOT' +ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' From 67d31fd7d9ec1426eab7c3a8ab37de7c7ffa5bd6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 14:29:52 +0100 Subject: [PATCH 165/898] Upgrade to Jacoco 0.7.7.201606060606 Closes gh-282 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e917b7b9e..498ea2f65 100644 --- a/build.gradle +++ b/build.gradle @@ -72,7 +72,7 @@ subprojects { dependency 'org.hibernate:hibernate-validator:5.2.2.Final' dependency 'org.mockito:mockito-core:1.10.19' dependency 'org.springframework.hateoas:spring-hateoas:0.19.0.RELEASE' - dependency 'org.jacoco:org.jacoco.agent:0.7.5.201505241946' + dependency 'org.jacoco:org.jacoco.agent:0.7.7.201606060606' } } From 5c74bef194f7ba8894ed1fe96d55b8dfe51174d8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 14:36:25 +0100 Subject: [PATCH 166/898] Upgrade to Gradle 2.14.1 Closes gh-283 --- gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53639 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5ccda13e9cb94678ba179b32452cf3d60dc36353..2c6137b87896c8f70315ae454e00a969ef5f6019 100644 GIT binary patch delta 1762 zcmY*Z3rv$&6u$inv`UK;1cg>AAP<3I*QyZ#iC_c)5wQ50V{{aR$v}Zv1viU2n4rAw zHXk9|7`Qrh0WIp7z(AnoQJ^@Taf|a2Ky)&#+2S6eyZ^ZaY?J0Y_xrzd&i9{t+M-%+ zaV=LE7tOVri4dQUq%m2QLN7jn$jkc8K9xaR9n3lA91fb6coNBJH!cfCAAsjl7O*ep z9*a6VCYJ%?kktbqvaIWX&^huQY=H5zyG0q^Y^gOcE1W7Q(?4$`4;Zfn8yz6nFBecv z*>WdaV6@@SXF^aDdz%(4Oytq@(oKncK5-G5byoW!9(y<9ji>AU6QoPxr45a;WtU`2 z6gV_lHe()9e0DOx*@W|xJ@zjxZ^`PA3J$4Tqh=RYi36P*^Zepe8K#S-S>rwp3&X39 zuKZ}+>)vk3-r#Ei%4f$sxB9LaS)HujDXe^7zUybEDXb?bcx~Y`;brDnieS8Bhu^@# zi)Z9XTNK{gM>K{StzFB8klihJ?`O`x`sU5gV-}8QjAZ)j*LUVPyIrqWC5`6yt(%p0 z#!9U_neDrDxGwN_=a*k;wk^K$kGyU~?NHyU+9nJB^N}>+YkRTL^G?swiAc@;FTQL~ z`1XawRDG*RRQ%WZ;oFL92X>j6^@g&SuiX}TQM^~_&n2ikt^9;x11wiP1VWPf3J9HB z`a>EBcVG@Ys?C(}A?V7Ja3Of04x)i)!B5t}{HOVsivK=vg9nVMWQa0#N6s>K?2tb` z)i`&%Jwke4EG<}opXS-<4wkF!K|N7prd`c-cWH24d&vqO9X-dT&2arw`l#r_JGAtu zZWYz|es7}8M3aJQ6wR2+XS+6(y0oqhaBl8O1e~L%byfNlIQQyfrgz!Zu=cgJ-DwD62Zb99BF+ccXmEwoxIx5J zE3tII8JmOq(M($4;qUt9gR}lV5%c%} zu0H3E1x8q5>}C`(ohA5AN$}LL4-@M65lHSf${=xqP;1Hw<%16o(kqGY7cu46L2-sK*z`-)^Mgj{S93bIJ-#)}7{ zz{0)(5mR`Mcn_F*_e*UJxyMPrGh_uUZ=|?>s-Jk!o!-izh{?Y|XfYO)&SGB{JckcC zjXol?+ecbkuF)?#sBv@9N5XoObLlMC-@c~YRNFxkX96ALjV35h+ zD2{+Zvr%sKpq9kbB<)Nun7`{umQR(Dsi}T|C`9JO>Vw(zJA~TI_KVuYjpZG z+B8T*o6JW@BtrITb&jc0L_i%~`zkKSYp2zVgy#u7G$%19lCotq1Dz`XUaAwwT(i>w5|IGYWyjL<^G2gcLpdzR^1yh8|#Qoh3q7N^|BtmgcB zn+3p>`n{YFi{dRqY{1k|A!|SPd8kN4s!)f^PcFq{d;J&2YXXB+l|ib?8aGv?n@14# ziEx`o6GiTzhieZ`j&L~To$VXfBp0Vmy}5Wp^hl6PU;14cSf?F4LOr=2!c)lmPR{1u zDu|oX7Zv@Lr+RI)lv?8i#nYqH7K;7@PqaF;TsM|BDF|A<&pCZVYww=A@fnfdZ+xlzjFDU^>CNsOu?nmF*6<(c_Rciezti0&#Gq>uXKk((<6E5o#Z*5wiMSJ#WJQ>MRNPjTyoj+O%YOZ#EY@Y zxE8V(YIpUNlAf;92(9O6CQ~5$Pev)squVHg(uq1!|U1A7>LvfxWxfaC^-+{d|q|wvzPb&IvbN3|`e$ z%T+-d9<_*OKk7`6oR^AY8r5N5$y(?44abxtArU4B*)KrIi(@cgRd)as_f5BiN+~D3 ze)#SWRk(?6uIMXX&PSPF)48_qzEw&>=iDo+C#Q(aQ2$x`Orv#GZ_eiJ# zJv27Z;|K?akyk!5&^N@pf#a28S+5#w2YV&d^gVVS_br&S2D*dL{ Date: Mon, 25 Jul 2016 14:53:54 +0100 Subject: [PATCH 167/898] Configure sonar.branch --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 498ea2f65..3881aab5d 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ apply plugin: 'org.sonarqube' sonarqube { properties { + property 'sonar.branch', '1.1.x' property 'sonar.jacoco.reportPath', "${buildDir.name}/jacoco.exec" property 'sonar.java.coveragePlugin', 'jacoco' property 'sonar.links.ci', 'https://build.spring.io/browse/SRD' From 10e421a105f4a1f818b27866dcf64dbce7be83a6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 15:39:12 +0100 Subject: [PATCH 168/898] Upgrade the samples that use Gradle to 2.14.1 as well See gh-283 --- .../gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53324 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-assured/gradlew | 46 ++++++++++-------- samples/rest-assured/gradlew.bat | 8 +-- .../gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53324 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-notes-slate/gradlew | 46 ++++++++++-------- samples/rest-notes-slate/gradlew.bat | 8 +-- .../gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53324 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-notes-spring-hateoas/gradlew | 46 ++++++++++-------- samples/rest-notes-spring-hateoas/gradlew.bat | 8 +-- .../testng/gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53324 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/testng/gradlew | 46 ++++++++++-------- samples/testng/gradlew.bat | 8 +-- 16 files changed, 124 insertions(+), 108 deletions(-) diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar b/samples/rest-assured/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5484 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 E2en2;ga7~l delta 5699 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 diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties index 72a999e4b..92953a8ce 100644 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 15 17:16:16 GMT 2016 +#Mon Jul 25 15:38:29 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew index 9d82f7891..27309d923 100755 --- a/samples/rest-assured/gradlew +++ b/samples/rest-assured/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat index 8a0b282aa..832fdb607 100644 --- a/samples/rest-assured/gradlew.bat +++ b/samples/rest-assured/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar index e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5494 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 MSIUeM4P@v40p5%%-2eap delta 5701 zcma)Ac|4Te`yP`m`!<%zmcrN>LPR0N*mo~Q$R5T{G+E0M@sJ8pS&}_V8vDLe^pYiO zmXIYisMPNsj+`46+0pB!&7EKC@`OW{XAf@}^ysbYruLJ81q z@?`>qrg%nxsyL}xC<&M(!+R+6k&FUN%L*2gLxdrp25q+bZ&v@EMJQAz#4a7Jpc*Q0%Rs-?M2SJ*+YRBEV zM*^6U3Pj)njeKFWjhK0bEHZGS&bdQRS>D0vp<2|0NRtu4G)|!i_VBl za;Z-J>iVf7WfX1xeP%khkj=l0{APjuYiA2HYGwB-B@1cUs+YZijLJ!$qgO`TXk0uU z@4wvE4QTgET)M+R6{2IyLtzV$4T`;gU>7^@fXO>QS?ZwHEui`a&BOO%|5A5%-IE1Y zo}5VQu-M@M?iLJ(Xwy5isc%!;xMsDq#~>Y7@@2V_G(R+53a|1Xuk5?j?eVo=D&Wjv zu1AKI=fg{l0%xAm3p}68(pSbcgy~)R$uk@r74}?5IdgX#V=b5;o2<}s_C8ZtbhXJ{ zaT=RqPG7J4E=+vk>7kvT?!_y}rs|X&CR+dbs)l@c($bTl3gq`iw|1R7#S~WsHKVvK zwA^?k7Y(T@yob^PaE09UEoa~Ja$UEIxm$B?A(h#pPyYtJg6^Z#gRR4y?@}wqWLXNg zGAHiPFepgd0T_*Q!?Th-%|ZxY)s)S7kuLN3wPe-kFZIGRDERh+smf$CDG~liud;O} zO~xA6l5@8h)xP|)QXl=QA(yO~uzd){3?B;a_fjWHsbv?e%w6=hV_x4c{}83R3S{2f z>Q#vrzMpF-^(1|AeM+9KGDT7z_I7NKDKn?~dPp=bZU!SztEzQ7Qq8Vsk8_G`y7l;L z+LWa&+Jl5uafcuF{A`G)>kPPPXp_L>6J{Fj%*vZj8-??5x~17>VTnGnL$g2jjEBSJ zSX07RU7&}-Z*4Rbqt&X71k@g=D}Q1rsAE0B)Frfb`_8na?jCazFLY%18&jwoOfxHM z_oJ=Ai}6pG9ij8JUg+r$U24;vs5-+zD#eWmC8~p~?+p4?XV|qlK08c*&=Qu|+Z|mO zQsK?LcB_ffRi%X`4^1OAnqU}dU|8-$zo0O~>yo0r57SU=Td-Z&8#8Xu#gwPhT8)He z*E?QGN%jkBo1*_sMS8>Lhe%^Bs)ZgJpv z!jk#d*cdDXP>lC`UlZjZd{=yeUB?um!@B+V#nSTcq6U+79v-K>MYpi^#T~J}tcbmE za%kEQN{+uoI;zQTeoTfoO-Y{G3ko%Y!VS)x_R-mMmPIw`pi z}GUp-(D#!5cYG-U^}hl<*HEQX)H@S zAp)1CU4d=zRO@yBtruh=cHf`khmcTr4mWSsj8aXxy~n{*)x}lZH4kgKFxlAj@HWJn z9COIDH2vPw$S$+JlE^o?nDoNQU(D_Ax^??$u_~n!Yymq`b1rA5?gdnE`DV~P-Q=#^ zP#+6QZ@lIqCWg(#W#KQoCTFA&S_{eu=;FUz-E6k7Xu$3^SL2Sl2uzW;Dg zbLlv%w@%$hPjo$uB4F@cz;piE+_T>w2*o8Xxs7pTdiFbT);7oY@nLK!MqrMbt-))h zf8XFhj}?VgJ8?*P46z6AJqAXbx#bK5O_zjmfxuk)CA`x#?; zzM*27M)^b;1Hy=N@yT_I+m;;P()rIbcl))rO@4nFV9IwTAYgQ7gd5qoff$HQbsUQ9 z`*C)i?p`J~GyCDdXO@?kJijhUuascZ%q~l-KngUR$3CWEBWUySUC|#JCPutjFLiNG zB{QAAGcbJXJr3jWG42dG$J?1~Mz`24`ndfJ%rE$_PE*cB%^5H2PQ&f^cna|^y5mnd z5nP=OUd8)Gt3T<^yJ)=>y?L^jc0WJfF7B&G!_&EedY8KQ`}%Cn<`|iXLTfBwfIE#E zx7Czs^3kyKf3}|;KfB1-u>VXWS!+idhH1+&FIhB)VOs5A)xp~LKS?Uvv-uVjbC7@Y zcfe?cWit!N^3Kr9lbRTl2=Qnnm*0_%nA`onwjUs4O{Khx?G};n#Lc%C z*@icNaVOsxu@lF*X%=?PI7gOml#D)FTpxQ5d2;^abAn=L(%af5%qUP2Cse(PVVK z?8h`BB7~odV9_%7h8mJ$U!;deQ=E2NQpDwsR{6glXAqCWoUS16kiLE({~Kj!JoIYx zArdY|eYfC-D0OoF4O!U5p%{6lY@pczwlQ%btSnNf>>Uy(ghRX1qY;i6ioun(Oe|I4 zs!-r+sEOe>Bd23jD+_ZxSM(4PxnH8gSmXbl)nI_eGbf75;tDgAk)6Om7S=3ttlx2_eVo|T**S5cX$ z0psRHaUXAvc;NbW#eg^a;U&AjdYx$_Y8ngU% z6l5bF@7SN#II%Ehpx4xYT#lAqf7pdXn*7~xy216a!(A=ckhIsldq0H_t894x^6A`V z%I`8ixBeEBnR7BiDs$4I@b1AX?`P)aEbM!=+bZiSJWp2oFIlSaIy>2$UDe7%7C3hS z{J-4ICu07Fb_{>@H{loeWhL1}}(B47oWav9V#WG^571P+b1#bz`uF zG6cNsL}g8f1`S09b<8j6==Trwi}wx;FPh*6hfS#sB#flxjHN58kuNKdNE4qxdIn=@ zq_o_UDDcSc#EB*7|HK~%hEYBKPHhO3g_8lpc6^xRL9cdxrs%B(T^M<7>wJsgD_wKR zxX@B|E9Hxn;w=_@&3QUF+~CGW^2E=DAsguj0eZ}FA=H+96=u}Mo17eaI}vxYKYod- z5s;6WTQ3|9bS;?svU=e~ue8W}Is4&K^)CIW-WNO1Lw!;CTGY>ZHnWDfrX%H+9`5_b zRou6+eGwX&&;PoeVfI0Sp|PKm39el!weNuW*BA7U>)W^+eK-0T)VZA#A2bSj7z9A>OHkK_~q+T9AxQ2m2r|Goenr3(1ki;*8qs-D>2hT>;{ zxZ^4T;&l2&fU4jwtHc?ldny@WHOqsSnlNkf<2^>0%>^nEXS|^!6a?%m=74Ci6Y-9U zfc3_`A!6AHFNn&hq(TeVn2!_}D*dHVIK$LNpJoqo}JTpQeCO<!Qg^jZiX8Nj9m4&^2Tp0vRLBQho+tA&n0Edg-|l!zY(RGGES6=21{D)!%WNPcwqM-3dSBMyMvw)1$W_!n^~2!f9JNI2r`cyW4m zI)J~O4o@a|O(!8YbK=RloU8!#0D-JHQ~TirD+ID30)g<8kU9DB)cnG^N z1quKLc$}`2xHDp%aHyX-9?RvV2i!VO6SyfL9&U-pjmW5hzD^dtqh0%R6@t!AkvRMt z_|b{vC%xAo5HPMkPj!TgJ)qwuboq#MgdT0g(Zfg>!2e8;Lp6`9gyZVsd)3MFnv|ob z8_CH%GQQXI_5L}~+7#SInLoxyTm9%N@TTiLJ`AMYwd4YDaTWYSkdOqOfVqAFz}Ar( zu~W!sARqd`BV#@ktQpxPf`X}c(tKnO2Br2 z3m=a5Tjz~bkemhX1}S+l?@y9V1DwR748WmXh2SCK0vHFS@d7}01FQiQ;0Fc7N%Fyz zfJqAgB-P6vghOM>{|L92vHz)_F!?1qF0e2zN1>Al1-}FhxPc2pCkRp44Z)%BtMDR> zRouYqA;P``5aowQmHi0{oFx(9t^;IVDgRqp>0vl@>os1$YnYcHdJn|38t~ZAurNNg bKi@x~80mdC-3;tBodo3HFj3mK9J%^G#jsYK diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties index 90a0f04cd..01d6474a9 100644 --- a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 15 14:25:16 BST 2015 +#Mon Jul 25 15:38:07 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/samples/rest-notes-slate/gradlew b/samples/rest-notes-slate/gradlew index 97fac783e..27309d923 100755 --- a/samples/rest-notes-slate/gradlew +++ b/samples/rest-notes-slate/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-notes-slate/gradlew.bat b/samples/rest-notes-slate/gradlew.bat index 8a0b282aa..832fdb607 100644 --- a/samples/rest-notes-slate/gradlew.bat +++ b/samples/rest-notes-slate/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5484 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 E2en2;ga7~l delta 5699 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 diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties index 347aa08f3..403d06ec6 100644 --- a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 15 17:16:28 GMT 2016 +#Mon Jul 25 15:38:17 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/samples/rest-notes-spring-hateoas/gradlew b/samples/rest-notes-spring-hateoas/gradlew index 9d82f7891..27309d923 100755 --- a/samples/rest-notes-spring-hateoas/gradlew +++ b/samples/rest-notes-spring-hateoas/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-notes-spring-hateoas/gradlew.bat b/samples/rest-notes-spring-hateoas/gradlew.bat index 8a0b282aa..832fdb607 100644 --- a/samples/rest-notes-spring-hateoas/gradlew.bat +++ b/samples/rest-notes-spring-hateoas/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.jar b/samples/testng/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..3baa851b28c65f87dd36a6748e1a85cf360c1301 100644 GIT binary patch delta 5484 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 E2en2;ga7~l delta 5699 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 diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties index 9332fb8f6..8884c2783 100644 --- a/samples/testng/gradle/wrapper/gradle-wrapper.properties +++ b/samples/testng/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 15 17:16:46 GMT 2016 +#Mon Jul 25 15:38:37 BST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip diff --git a/samples/testng/gradlew b/samples/testng/gradlew index 9d82f7891..27309d923 100755 --- a/samples/testng/gradlew +++ b/samples/testng/gradlew @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/testng/gradlew.bat b/samples/testng/gradlew.bat index 8a0b282aa..832fdb607 100644 --- a/samples/testng/gradlew.bat +++ b/samples/testng/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args From 65571d98dd0fc9416d28885cce536a24f2c46cf4 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 25 Jul 2016 18:01:55 +0100 Subject: [PATCH 169/898] Revert "Upgrade to Gradle 2.14.1" This reverts commit 5c74bef194f7ba8894ed1fe96d55b8dfe51174d8 and commit 10e421a105f4a1f818b27866dcf64dbce7be83a6. See gh-283 --- gradle/wrapper/gradle-wrapper.jar | Bin 53639 -> 53638 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- .../gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-assured/gradlew | 46 ++++++++---------- samples/rest-assured/gradlew.bat | 8 +-- .../gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 53638 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-notes-slate/gradlew | 46 ++++++++---------- samples/rest-notes-slate/gradlew.bat | 8 +-- .../gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/rest-notes-spring-hateoas/gradlew | 46 ++++++++---------- samples/rest-notes-spring-hateoas/gradlew.bat | 8 +-- .../testng/gradle/wrapper/gradle-wrapper.jar | Bin 53324 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- samples/testng/gradlew | 46 ++++++++---------- samples/testng/gradlew.bat | 8 +-- 18 files changed, 110 insertions(+), 126 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c6137b87896c8f70315ae454e00a969ef5f6019..5ccda13e9cb94678ba179b32452cf3d60dc36353 100644 GIT binary patch delta 1839 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 delta 5484 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 E2en2;ga7~l diff --git a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties index 92953a8ce..72a999e4b 100644 --- a/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-assured/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 25 15:38:29 BST 2016 +#Mon Feb 15 17:16:16 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-assured/gradlew b/samples/rest-assured/gradlew index 27309d923..9d82f7891 100755 --- a/samples/rest-assured/gradlew +++ b/samples/rest-assured/gradlew @@ -6,30 +6,12 @@ ## ############################################################################## -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -48,7 +30,6 @@ die ( ) { cygwin=false msys=false darwin=false -nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -59,11 +40,26 @@ case "`uname`" in MINGW* ) msys=true ;; - NONSTOP* ) - nonstop=true - ;; esac +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -89,7 +85,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-assured/gradlew.bat b/samples/rest-assured/gradlew.bat index 832fdb607..8a0b282aa 100644 --- a/samples/rest-assured/gradlew.bat +++ b/samples/rest-assured/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windows variants +@rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.jar index 3baa851b28c65f87dd36a6748e1a85cf360c1301..e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2 100644 GIT binary patch delta 5701 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 bKi@x~80mdC-3;tBodo3HFj3mK9J%^G#jsYK delta 5494 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 MSIUeM4P@v40p5%%-2eap diff --git a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties index 01d6474a9..90a0f04cd 100644 --- a/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-slate/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 25 15:38:07 BST 2016 +#Thu Oct 15 14:25:16 BST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip diff --git a/samples/rest-notes-slate/gradlew b/samples/rest-notes-slate/gradlew index 27309d923..97fac783e 100755 --- a/samples/rest-notes-slate/gradlew +++ b/samples/rest-notes-slate/gradlew @@ -6,30 +6,12 @@ ## ############################################################################## -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -48,7 +30,6 @@ die ( ) { cygwin=false msys=false darwin=false -nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -59,11 +40,26 @@ case "`uname`" in MINGW* ) msys=true ;; - NONSTOP* ) - nonstop=true - ;; esac +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -89,7 +85,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-notes-slate/gradlew.bat b/samples/rest-notes-slate/gradlew.bat index 832fdb607..8a0b282aa 100644 --- a/samples/rest-notes-slate/gradlew.bat +++ b/samples/rest-notes-slate/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windows variants +@rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.jar index 3baa851b28c65f87dd36a6748e1a85cf360c1301..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 5699 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 delta 5484 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 E2en2;ga7~l diff --git a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties index 403d06ec6..347aa08f3 100644 --- a/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties +++ b/samples/rest-notes-spring-hateoas/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 25 15:38:17 BST 2016 +#Mon Feb 15 17:16:28 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/rest-notes-spring-hateoas/gradlew b/samples/rest-notes-spring-hateoas/gradlew index 27309d923..9d82f7891 100755 --- a/samples/rest-notes-spring-hateoas/gradlew +++ b/samples/rest-notes-spring-hateoas/gradlew @@ -6,30 +6,12 @@ ## ############################################################################## -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -48,7 +30,6 @@ die ( ) { cygwin=false msys=false darwin=false -nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -59,11 +40,26 @@ case "`uname`" in MINGW* ) msys=true ;; - NONSTOP* ) - nonstop=true - ;; esac +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -89,7 +85,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/rest-notes-spring-hateoas/gradlew.bat b/samples/rest-notes-spring-hateoas/gradlew.bat index 832fdb607..8a0b282aa 100644 --- a/samples/rest-notes-spring-hateoas/gradlew.bat +++ b/samples/rest-notes-spring-hateoas/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windows variants +@rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.jar b/samples/testng/gradle/wrapper/gradle-wrapper.jar index 3baa851b28c65f87dd36a6748e1a85cf360c1301..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 5699 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 delta 5484 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 E2en2;ga7~l diff --git a/samples/testng/gradle/wrapper/gradle-wrapper.properties b/samples/testng/gradle/wrapper/gradle-wrapper.properties index 8884c2783..9332fb8f6 100644 --- a/samples/testng/gradle/wrapper/gradle-wrapper.properties +++ b/samples/testng/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 25 15:38:37 BST 2016 +#Mon Feb 15 17:16:46 GMT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/samples/testng/gradlew b/samples/testng/gradlew index 27309d923..9d82f7891 100755 --- a/samples/testng/gradlew +++ b/samples/testng/gradlew @@ -6,30 +6,12 @@ ## ############################################################################## -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -48,7 +30,6 @@ die ( ) { cygwin=false msys=false darwin=false -nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -59,11 +40,26 @@ case "`uname`" in MINGW* ) msys=true ;; - NONSTOP* ) - nonstop=true - ;; esac +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -89,7 +85,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/samples/testng/gradlew.bat b/samples/testng/gradlew.bat index 832fdb607..8a0b282aa 100644 --- a/samples/testng/gradlew.bat +++ b/samples/testng/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windows variants +@rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args From f2717363f034d898f3a33d0a964a24ff0638da8f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 26 Jul 2016 12:09:20 +0100 Subject: [PATCH 170/898] Raise the minimum Spring Framework version to 4.3.x Closes gh-284 --- build.gradle | 4 ++-- docs/src/docs/asciidoc/getting-started.adoc | 2 +- samples/rest-assured/build.gradle | 1 + samples/rest-notes-grails/build.gradle | 1 + samples/rest-notes-slate/build.gradle | 1 + samples/rest-notes-spring-data-rest/pom.xml | 1 + samples/rest-notes-spring-hateoas/build.gradle | 1 + samples/testng/build.gradle | 2 ++ .../restdocs/operation/AbstractOperationMessage.java | 2 +- .../operation/preprocess/PatternReplacingContentModifier.java | 4 ++-- .../preprocess/UriModifyingOperationPreprocessor.java | 4 ++-- 11 files changed, 15 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 498ea2f65..330e83554 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ sonarqube { } ext { - springVersion = '4.2.5.RELEASE' + springVersion = '4.3.1.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", @@ -60,7 +60,7 @@ subprojects { mavenBom "org.springframework:spring-framework-bom:$springVersion" } dependencies { - dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6.1' + dependency 'com.fasterxml.jackson.core:jackson-databind:2.5.5' dependency 'com.jayway.restassured:rest-assured:2.8.0' dependency 'com.samskivert:jmustache:1.12' dependency 'commons-codec:commons-codec:1.10' diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 0dd076206..9b0040ab3 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -68,7 +68,7 @@ If you want to jump straight in, a number of sample applications are available: Spring REST Docs has the following minimum requirements: - Java 7 -- Spring Framework 4.2 +- Spring Framework 4.3 Additionally, the `spring-restdocs-restassured` module has the following minimum requirements: diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index 4d5bb5237..ea67c8610 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -31,6 +31,7 @@ ext { } ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' +ext['spring.version']='4.3.1.RELEASE' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 535ba3150..52ef83950 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -47,6 +47,7 @@ dependencyManagement { } imports { mavenBom "org.grails:grails-bom:$grailsVersion" + mavenBom "org.springframework:spring-framework-bom:4.3.1.RELEASE" } applyMavenExclusions false } diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 91aea3b80..157e71255 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -27,6 +27,7 @@ ext { } ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' +ext['spring.version']='4.3.1.RELEASE' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 21699808e..cae3b9a8a 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -18,6 +18,7 @@ UTF-8 1.7 + 4.3.1.RELEASE 1.2.0.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 1be4c82f1..89a457416 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -30,6 +30,7 @@ ext { snippetsDir = file('build/generated-snippets') } +ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index f1e791c84..b0be86971 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -29,6 +29,8 @@ targetCompatibility = 1.7 ext { snippetsDir = file('build/generated-snippets') } + +ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java index 41e8fec3e..75c48e665 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/AbstractOperationMessage.java @@ -63,7 +63,7 @@ private Charset extractCharsetFromContentTypeHeader() { if (contentType == null) { return null; } - return contentType.getCharSet(); + return contentType.getCharset(); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java index 7fe67754c..f33f6be66 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/PatternReplacingContentModifier.java @@ -49,8 +49,8 @@ class PatternReplacingContentModifier implements ContentModifier { @Override public byte[] modifyContent(byte[] content, MediaType contentType) { String original; - if (contentType != null && contentType.getCharSet() != null) { - original = new String(content, contentType.getCharSet()); + if (contentType != null && contentType.getCharset() != null) { + original = new String(content, contentType.getCharset()); } else { original = new String(content); diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java index 1a4798147..609729437 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/operation/preprocess/UriModifyingOperationPreprocessor.java @@ -203,8 +203,8 @@ private void setPort(String port) { @Override public byte[] modifyContent(byte[] content, MediaType contentType) { String input; - if (contentType != null && contentType.getCharSet() != null) { - input = new String(content, contentType.getCharSet()); + if (contentType != null && contentType.getCharset() != null) { + input = new String(content, contentType.getCharset()); } else { input = new String(content); From bf9b0f2d640aee805f9f3b097573f95753d344c5 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 29 Jul 2016 21:10:21 +0100 Subject: [PATCH 171/898] Ensure that query string is not duplicated when parameters overlap Closes gh-286 --- .../restdocs/cli/CliOperationRequest.java | 28 ------- .../restdocs/cli/CurlRequestSnippet.java | 14 ++-- .../restdocs/cli/HttpieRequestSnippet.java | 24 +++--- .../restdocs/http/HttpRequestSnippet.java | 9 ++- .../restdocs/operation/Parameters.java | 35 ++++++++ .../restdocs/cli/CurlRequestSnippetTests.java | 72 +++++++++++++++-- .../cli/HttpieRequestSnippetTests.java | 72 +++++++++++++++-- .../http/HttpRequestSnippetTests.java | 81 +++++++++++++++++++ 8 files changed, 276 insertions(+), 59 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index ce305278a..b88994104 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -52,34 +52,6 @@ final class CliOperationRequest implements OperationRequest { new BasicAuthHeaderFilter(), new HostHeaderFilter(delegate.getUri()))); } - Parameters getUniqueParameters() { - Parameters queryStringParameters = new QueryStringParser() - .parse(this.delegate.getUri()); - Parameters uniqueParameters = new Parameters(); - - for (Map.Entry> parameter : this.delegate.getParameters() - .entrySet()) { - addIfUnique(parameter, queryStringParameters, uniqueParameters); - } - return uniqueParameters; - } - - private void addIfUnique(Map.Entry> parameter, - Parameters queryStringParameters, Parameters uniqueParameters) { - if (!queryStringParameters.containsKey(parameter.getKey())) { - uniqueParameters.put(parameter.getKey(), parameter.getValue()); - } - else { - List candidates = parameter.getValue(); - List existing = queryStringParameters.get(parameter.getKey()); - for (String candidate : candidates) { - if (!existing.contains(candidate)) { - uniqueParameters.add(parameter.getKey(), candidate); - } - } - } - } - boolean isPutOrPost() { return HttpMethod.PUT.equals(this.delegate.getMethod()) || HttpMethod.POST.equals(this.delegate.getMethod()); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 31127c3c4..4f8d603cf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -70,9 +70,12 @@ protected Map createModel(Operation operation) { private String getUrl(Operation operation) { OperationRequest request = operation.getRequest(); - if (!request.getParameters().isEmpty() && includeParametersInUri(request)) { - return String.format("'%s?%s'", request.getUri(), - request.getParameters().toQueryString()); + Parameters uniqueParameters = request.getParameters() + .getUniqueParameters(operation.getRequest().getUri()); + if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { + return String.format("'%s%s%s'", request.getUri(), + StringUtils.hasText(request.getUri().getRawQuery()) ? "&" : "?", + uniqueParameters.toQueryString()); } return String.format("'%s'", request.getUri()); } @@ -157,9 +160,10 @@ else if (request.isPutOrPost()) { } } - private void writeContentUsingParameters(CliOperationRequest request, + private void writeContentUsingParameters(OperationRequest request, PrintWriter writer) { - Parameters uniqueParameters = request.getUniqueParameters(); + Parameters uniqueParameters = request.getParameters() + .getUniqueParameters(request.getUri()); String queryString = uniqueParameters.toQueryString(); if (StringUtils.hasText(queryString)) { writer.print(String.format(" -d '%s'", queryString)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 4c95c04d1..10b96facb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -90,10 +90,13 @@ private String getOptions(CliOperationRequest request) { return options.toString(); } - private String getUrl(CliOperationRequest request) { - if (!request.getUniqueParameters().isEmpty() && includeParametersInUri(request)) { - return String.format("'%s?%s'", request.getUri(), - request.getParameters().toQueryString()); + private String getUrl(OperationRequest request) { + Parameters uniqueParameters = request.getParameters() + .getUniqueParameters(request.getUri()); + if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { + return String.format("'%s%s%s'", request.getUri(), + StringUtils.hasText(request.getUri().getRawQuery()) ? "&" : "?", + uniqueParameters.toQueryString()); } return String.format("'%s'", request.getUri()); } @@ -107,14 +110,15 @@ private String getRequestItems(CliOperationRequest request) { return requestItems.toString(); } - private void writeOptions(CliOperationRequest request, PrintWriter writer) { - if (!request.getParts().isEmpty() || (!request.getUniqueParameters().isEmpty() - && !includeParametersInUri(request))) { + private void writeOptions(OperationRequest request, PrintWriter writer) { + if (!request.getParts().isEmpty() + || (!request.getParameters().getUniqueParameters(request.getUri()) + .isEmpty() && !includeParametersInUri(request))) { writer.print("--form "); } } - private boolean includeParametersInUri(CliOperationRequest request) { + private boolean includeParametersInUri(OperationRequest request) { return request.getMethod() == HttpMethod.GET || request.getContent().length > 0; } @@ -167,7 +171,9 @@ private void writeParametersIfNecessary(CliOperationRequest request, writeContentUsingParameters(request.getParameters(), writer); } else if (request.isPutOrPost()) { - writeContentUsingParameters(request.getUniqueParameters(), writer); + writeContentUsingParameters( + request.getParameters().getUniqueParameters(request.getUri()), + writer); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index c966f137e..0110dd1b6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -30,6 +30,7 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; @@ -75,12 +76,14 @@ protected Map createModel(Operation operation) { private String getPath(OperationRequest request) { String path = request.getUri().getRawPath(); String queryString = request.getUri().getRawQuery(); - if (!request.getParameters().isEmpty() && includeParametersInUri(request)) { + Parameters uniqueParameters = request.getParameters() + .getUniqueParameters(request.getUri()); + if (!uniqueParameters.isEmpty() && includeParametersInUri(request)) { if (StringUtils.hasText(queryString)) { - queryString = queryString + "&" + request.getParameters().toQueryString(); + queryString = queryString + "&" + uniqueParameters.toQueryString(); } else { - queryString = request.getParameters().toQueryString(); + queryString = uniqueParameters.toQueryString(); } } if (StringUtils.hasText(queryString)) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java index b696c2927..bd05f4216 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -17,10 +17,12 @@ package org.springframework.restdocs.operation; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.net.URLEncoder; import java.util.List; import java.util.Map; +import org.springframework.restdocs.cli.QueryStringParser; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.StringUtils; @@ -53,6 +55,39 @@ public String toQueryString() { return sb.toString(); } + /** + * Returns a new {@code Parameters} containing only the parameters that do no appear + * in the query string of the given {@code uri}. + * + * @param uri the uri + * @return the unique parameters + */ + public Parameters getUniqueParameters(URI uri) { + Parameters queryStringParameters = new QueryStringParser().parse(uri); + Parameters uniqueParameters = new Parameters(); + + for (Map.Entry> parameter : entrySet()) { + addIfUnique(parameter, queryStringParameters, uniqueParameters); + } + return uniqueParameters; + } + + private void addIfUnique(Map.Entry> parameter, + Parameters queryStringParameters, Parameters uniqueParameters) { + if (!queryStringParameters.containsKey(parameter.getKey())) { + uniqueParameters.put(parameter.getKey(), parameter.getValue()); + } + else { + List candidates = parameter.getValue(); + List existing = queryStringParameters.get(parameter.getKey()); + for (String candidate : candidates) { + if (!existing.contains(candidate)) { + uniqueParameters.add(parameter.getKey(), candidate); + } + } + } + } + private static void append(StringBuilder sb, String key) { append(sb, key, ""); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index e26ca2176..314234b85 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -95,6 +95,47 @@ public void getRequestWithQueryString() throws IOException { .request("http://localhost/foo?param=value").build()); } + @Test + public void getRequestWithTotallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectCurlRequest( + "request-with-totally-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i")); + new CurlRequestSnippet().document(operationBuilder( + "request-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?param=value") + .param("param", "value").build()); + } + + @Test + public void getRequestWithPartiallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectCurlRequest( + "request-with-partially-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); + new CurlRequestSnippet().document(operationBuilder( + "request-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("a", "alpha") + .param("b", "bravo").build()); + } + + @Test + public void getRequestWithDisjointQueryStringAndParameters() throws IOException { + this.snippet + .expectCurlRequest( + "request-with-partially-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); + new CurlRequestSnippet().document(operationBuilder( + "request-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("b", "bravo") + .build()); + } + @Test public void getRequestWithQueryStringWithNoValue() throws IOException { this.snippet.expectCurlRequest("request-with-query-string-with-no-value") @@ -172,25 +213,42 @@ public void postRequestWithUrlEncodedParameter() throws IOException { } @Test - public void postRequestWithQueryStringAndParameter() throws IOException { - this.snippet.expectCurlRequest("post-request-with-query-string-and-parameter") + public void postRequestWithDisjointQueryStringAndParameter() throws IOException { + this.snippet + .expectCurlRequest( + "post-request-with-disjoint-query-string-and-parameter") .withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet() - .document(operationBuilder("post-request-with-query-string-and-parameter") + new CurlRequestSnippet().document( + operationBuilder("post-request-with-disjoint-query-string-and-parameter") .request("http://localhost/foo?a=alpha").method("POST") .param("b", "bravo").build()); } @Test - public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + public void postRequestWithTotallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectCurlRequest( + "post-request-with-totally-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); + new CurlRequestSnippet().document(operationBuilder( + "post-request-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha&b=bravo").method("POST") + .param("a", "alpha").param("b", "bravo").build()); + } + + @Test + public void postRequestWithPartiallyOverlappingQueryStringAndParameters() + throws IOException { this.snippet .expectCurlRequest( - "post-request-with-overlapping-query-string-and-parameters") + "post-request-with-partially-overlapping-query-string-and-parameters") .withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); new CurlRequestSnippet().document(operationBuilder( - "post-request-with-overlapping-query-string-and-parameters") + "post-request-with-partially-overlapping-query-string-and-parameters") .request("http://localhost/foo?a=alpha").method("POST") .param("a", "alpha").param("b", "bravo").build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 9f8164b39..9dd297c6f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -96,6 +96,47 @@ public void getRequestWithQueryString() throws IOException { .request("http://localhost/foo?param=value").build()); } + @Test + public void getRequestWithTotallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectHttpieRequest( + "request-with-totally-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document(operationBuilder( + "request-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?param=value") + .param("param", "value").build()); + } + + @Test + public void getRequestWithPartiallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectHttpieRequest( + "request-with-partially-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document(operationBuilder( + "request-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("a", "alpha") + .param("b", "bravo").build()); + } + + @Test + public void getRequestWithDisjointQueryStringAndParameters() throws IOException { + this.snippet + .expectHttpieRequest( + "request-with-partially-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document(operationBuilder( + "request-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("b", "bravo") + .build()); + } + @Test public void getRequestWithQueryStringWithNoValue() throws IOException { this.snippet.expectHttpieRequest("request-with-query-string-with-no-value") @@ -173,25 +214,42 @@ public void postRequestWithUrlEncodedParameter() throws IOException { } @Test - public void postRequestWithQueryStringAndParameter() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-query-string-and-parameter") + public void postRequestWithDisjointQueryStringAndParameter() throws IOException { + this.snippet + .expectHttpieRequest( + "post-request-with-disjoint-query-string-and-parameter") .withContents(codeBlock("bash").content( "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-query-string-and-parameter") + new HttpieRequestSnippet().document( + operationBuilder("post-request-with-disjoint-query-string-and-parameter") .request("http://localhost/foo?a=alpha").method("POST") .param("b", "bravo").build()); } @Test - public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + public void postRequestWithTotallyOverlappingQueryStringAndParameters() + throws IOException { + this.snippet + .expectHttpieRequest( + "post-request-with-totally-overlapping-query-string-and-parameters") + .withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document(operationBuilder( + "post-request-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha&b=bravo").method("POST") + .param("a", "alpha").param("b", "bravo").build()); + } + + @Test + public void postRequestWithPartiallyOverlappingQueryStringAndParameters() + throws IOException { this.snippet .expectHttpieRequest( - "post-request-with-overlapping-query-string-and-parameters") + "post-request-with-partially-overlapping-query-string-and-parameters") .withContents(codeBlock("bash").content( "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); new HttpieRequestSnippet().document(operationBuilder( - "post-request-with-overlapping-query-string-and-parameters") + "post-request-with-partially-overlapping-query-string-and-parameters") .request("http://localhost/foo?a=alpha").method("POST") .param("a", "alpha").param("b", "bravo").build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 6f7057b37..e66e6b5be 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -102,6 +102,34 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { .request("http://localhost/foo?bar").build()); } + @Test + public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectHttpRequest( + "get-with-partially-overlapping-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") + .header(HttpHeaders.HOST, "localhost")); + + new HttpRequestSnippet().document(operationBuilder( + "get-with-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha").param("a", "alpha") + .param("b", "bravo").build()); + } + + @Test + public void getWithTotallyOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectHttpRequest( + "get-with-totally-overlapping-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") + .header(HttpHeaders.HOST, "localhost")); + + new HttpRequestSnippet().document(operationBuilder( + "get-with-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?a=alpha&b=bravo") + .param("a", "alpha").param("b", "bravo").build()); + } + @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; @@ -128,6 +156,59 @@ public void postRequestWithContentAndParameters() throws IOException { .param("a", "alpha").content(content).build()); } + @Test + public void postRequestWithContentAndDisjointQueryStringAndParameters() + throws IOException { + String content = "Hello, world"; + this.snippet + .expectHttpRequest( + "post-request-with-content-and-disjoint-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + + new HttpRequestSnippet().document(operationBuilder( + "post-request-with-content-and-disjoint-query-string-and-parameters") + .request("http://localhost/foo?b=bravo").method("POST") + .param("a", "alpha").content(content).build()); + } + + @Test + public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameters() + throws IOException { + String content = "Hello, world"; + this.snippet + .expectHttpRequest( + "post-request-with-content-and-partially-overlapping-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + + new HttpRequestSnippet().document(operationBuilder( + "post-request-with-content-and-partially-overlapping-query-string-and-parameters") + .request("http://localhost/foo?b=bravo").method("POST") + .param("a", "alpha").param("b", "bravo").content(content) + .build()); + } + + @Test + public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters() + throws IOException { + String content = "Hello, world"; + this.snippet + .expectHttpRequest( + "post-request-with-content-and-totally-overlapping-query-string-and-parameters") + .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + + new HttpRequestSnippet().document(operationBuilder( + "post-request-with-content-and-totally-overlapping-query-string-and-parameters") + .request("http://localhost/foo?b=bravo&a=alpha").method("POST") + .param("a", "alpha").param("b", "bravo").content(content) + .build()); + } + @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; From b430a887965f3cb4c2f15060e67ce5b666538333 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 10 Aug 2016 17:08:18 +0100 Subject: [PATCH 172/898] Polishing --- .../restdocs/curl/CurlRequestSnippet.java | 5 +---- .../org/springframework/restdocs/hypermedia/Link.java | 7 ++----- .../springframework/restdocs/test/SnippetMatchers.java | 10 +++++++--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 4f4ea07b7..50764ecde 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -223,10 +223,7 @@ private static final class BasicAuthHeaderFilter implements HeaderFilter { @Override public boolean allow(String name, List value) { - if (HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)) { - return false; - } - return true; + return !(HttpHeaders.AUTHORIZATION.equals(name) && isBasicAuthHeader(value)); } static boolean isBasicAuthHeader(List value) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java index 112886f0b..5e8a34f3b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/hypermedia/Link.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -80,10 +80,7 @@ public boolean equals(Object obj) { if (!this.href.equals(other.href)) { return false; } - if (!this.rel.equals(other.rel)) { - return false; - } - return true; + return this.rel.equals(other.rel); } @Override diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 98c1ccd10..7f23ad8ea 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -82,10 +82,14 @@ protected void addLine(String line) { } protected void addLine(int index, String line) { - if (index < 0) { - index = index + this.lines.size(); + this.lines.add(determineIndex(index), line); + } + + private int determineIndex(int index) { + if (index >= 0) { + return index; } - this.lines.add(index, line); + return index + this.lines.size(); } @Override From 9ba0a9b6be0b9be039382ee6ef8efb5073df1579 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 19 Aug 2016 19:30:22 +0100 Subject: [PATCH 173/898] Stop field snippets requiring a type for missing, optional, ignored field Missing optional fields require a type to be explicitly provided as it cannot be inferred from the payload. Ignored fields are not included in the documentation, yet, previously, a field that was missing, optional, and ignored would still require a type to be provided otherwise the test would fail. This was pointless as the field wasn't going to appear in the documentation. This commit updates the fields snippets so that a type is no longer required for a field that is missing, optional and ignored. Closes gh-289 --- .../payload/AbstractFieldsSnippet.java | 20 ++++++++++--------- .../payload/RequestFieldsSnippetTests.java | 15 ++++++++++++++ .../payload/ResponseFieldsSnippetTests.java | 14 +++++++++++++ 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index a21bf524c..16f7ba61b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -100,15 +100,17 @@ protected Map createModel(Operation operation) { validateFieldDocumentation(contentHandler); for (FieldDescriptor descriptor : this.fieldDescriptors) { - try { - descriptor.type(contentHandler.determineFieldType(descriptor)); - } - catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" - + descriptor.getPath() + "' as it is not present in the " - + "payload. Please provide a type using " - + "FieldDescriptor.type(Object type)."; - throw new FieldTypeRequiredException(message); + if (!descriptor.isIgnored()) { + try { + descriptor.type(contentHandler.determineFieldType(descriptor)); + } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + + descriptor.getPath() + "' as it is not present in the " + + "payload. Please provide a type using " + + "FieldDescriptor.type(Object type)."; + throw new FieldTypeRequiredException(message); + } } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 32e34be9e..18e89a9b0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -118,6 +118,21 @@ public void missingOptionalRequestField() throws IOException { .request("http://localhost").content("{}").build()); } + @Test + public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() + throws IOException { + this.snippet + .expectRequestFields( + "missing-ignored-optional-request-field-does-not-require-a-type") + .withContents(tableWithHeader("Path", "Type", "Description")); + new RequestFieldsSnippet(Arrays + .asList(fieldWithPath("a.b").description("one").ignored().optional())) + .document(operationBuilder( + "missing-ignored-optional-request-field-does-not-require-a-type") + .request("http://localhost").content("{}") + .build()); + } + @Test public void presentOptionalRequestField() throws IOException { this.snippet.expectRequestFields("present-optional-request-field") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index e0525888d..8d93a9872 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -147,6 +147,20 @@ public void missingOptionalResponseField() throws IOException { .response().content("{}").build()); } + @Test + public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() + throws IOException { + this.snippet + .expectResponseFields( + "missing-ignored-optional-response-field-does-not-require-a-type") + .withContents(tableWithHeader("Path", "Type", "Description")); + new ResponseFieldsSnippet(Arrays + .asList(fieldWithPath("a.b").description("one").ignored().optional())) + .document(operationBuilder( + "missing-ignored-optional-response-field-does-not-require-a-type") + .response().content("{}").build()); + } + @Test public void presentOptionalResponseField() throws IOException { this.snippet.expectResponseFields("present-optional-response-field") From a0faebdb0a33f2ccede8c1293a778d78ddccd7fb Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 19 Aug 2016 19:51:16 +0100 Subject: [PATCH 174/898] Ignore query string when extracting path parameters Closes gh-285 --- .../restdocs/request/PathParametersSnippet.java | 2 +- .../request/PathParametersSnippetTests.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index afbd0b527..64df77ec5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -116,7 +116,7 @@ private String removeQueryStringIfPresent(String urlTemplate) { @Override protected Set extractActualParameters(Operation operation) { - String urlTemplate = extractUrlTemplate(operation); + String urlTemplate = removeQueryStringIfPresent(extractUrlTemplate(operation)); Matcher matcher = NAMES_PATTERN.matcher(urlTemplate); Set actualParameters = new HashSet<>(); while (matcher.find()) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index e09809918..fbc7449b5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -130,6 +130,23 @@ public void pathParametersWithQueryString() throws IOException { "/{a}/{b}?foo=bar").build()); } + @Test + public void pathParametersWithQueryStringWithParameters() throws IOException { + this.snippet + .expectPathParameters("path-parameters-with-query-string-with-parameters") + .withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); + new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), + parameterWithName("b").description("two"))) + .document(operationBuilder( + "path-parameters-with-query-string-with-parameters") + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}?foo={c}") + .build()); + } + @Test public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); From 5c6cf7f12ff43ed97dcc2e59e1b63465d91ee175 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 19 Aug 2016 19:51:37 +0100 Subject: [PATCH 175/898] Polishing --- .../restdocs/request/PathParametersSnippet.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java index 64df77ec5..254cb7b42 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/PathParametersSnippet.java @@ -129,9 +129,8 @@ protected Set extractActualParameters(Operation operation) { private String extractUrlTemplate(Operation operation) { String urlTemplate = (String) operation.getAttributes() .get(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE); - Assert.notNull(urlTemplate, - "urlTemplate not found. If you are using MockMvc, did you use RestDocumentationRequestBuilders to " - + "build the request?"); + Assert.notNull(urlTemplate, "urlTemplate not found. If you are using MockMvc did " + + "you use RestDocumentationRequestBuilders to build the request?"); return urlTemplate; } From 14dcdb61d06fddbe40bc31f9bab2d00e8de73b99 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 19 Aug 2016 20:14:16 +0100 Subject: [PATCH 176/898] Fix package tangle between cli and operation packages QueryStringParser has been moved into the operation page. In the unlikely event that there were any external users of the class, a deprecated version remains in the cli package for backwards compatibility. It will be removed in 1.2. Closes gh-286 --- .../restdocs/cli/QueryStringParser.java | 73 +-------------- .../restdocs/operation/Parameters.java | 1 - .../restdocs/operation/QueryStringParser.java | 91 +++++++++++++++++++ .../QueryStringParserTests.java | 6 +- 4 files changed, 98 insertions(+), 73 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java rename spring-restdocs-core/src/test/java/org/springframework/restdocs/{cli => operation}/QueryStringParserTests.java (94%) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java index 4a3ab8ca1..2ebeed543 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/QueryStringParser.java @@ -16,78 +16,15 @@ package org.springframework.restdocs.cli; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLDecoder; -import java.util.LinkedList; -import java.util.List; -import java.util.Scanner; - -import org.springframework.restdocs.operation.Parameters; - /** * A parser for the query string of a URI. * * @author Andy Wilkinson + * @deprecated since 1.1.2 in favor of + * {@link org.springframework.restdocs.operation.QueryStringParser} */ -public class QueryStringParser { - - /** - * Parses the query string of the given {@code uri} and returns the resulting - * {@link Parameters}. - * - * @param uri the uri to parse - * @return the parameters parsed from the query string - */ - public Parameters parse(URI uri) { - String query = uri.getRawQuery(); - if (query != null) { - return parse(query); - } - return new Parameters(); - } - - private Parameters parse(String query) { - Parameters parameters = new Parameters(); - try (Scanner scanner = new Scanner(query)) { - scanner.useDelimiter("&"); - while (scanner.hasNext()) { - processParameter(scanner.next(), parameters); - } - } - return parameters; - } - - private void processParameter(String parameter, Parameters parameters) { - String[] components = parameter.split("="); - if (components.length > 0 && components.length < 3) { - if (components.length == 2) { - String name = components[0]; - String value = components[1]; - parameters.add(decode(name), decode(value)); - } - else { - List values = parameters.get(components[0]); - if (values == null) { - parameters.put(components[0], new LinkedList()); - } - } - } - else { - throw new IllegalArgumentException( - "The parameter '" + parameter + "' is malformed"); - } - } - - private String decode(String encoded) { - try { - return URLDecoder.decode(encoded, "UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw new IllegalStateException( - "Unable to URL encode " + encoded + " using UTF-8", ex); - } - - } +@Deprecated +public class QueryStringParser + extends org.springframework.restdocs.operation.QueryStringParser { } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java index bd05f4216..f2ec42a5e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/Parameters.java @@ -22,7 +22,6 @@ import java.util.List; import java.util.Map; -import org.springframework.restdocs.cli.QueryStringParser; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.StringUtils; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java new file mode 100644 index 000000000..c5ab3b703 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryStringParser.java @@ -0,0 +1,91 @@ +/* + * Copyright 2014-2016 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.operation; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; + +/** + * A parser for the query string of a URI. + * + * @author Andy Wilkinson + */ +public class QueryStringParser { + + /** + * Parses the query string of the given {@code uri} and returns the resulting + * {@link Parameters}. + * + * @param uri the uri to parse + * @return the parameters parsed from the query string + */ + public Parameters parse(URI uri) { + String query = uri.getRawQuery(); + if (query != null) { + return parse(query); + } + return new Parameters(); + } + + private Parameters parse(String query) { + Parameters parameters = new Parameters(); + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private void processParameter(String parameter, Parameters parameters) { + String[] components = parameter.split("="); + if (components.length > 0 && components.length < 3) { + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + List values = parameters.get(components[0]); + if (values == null) { + parameters.put(components[0], new LinkedList()); + } + } + } + else { + throw new IllegalArgumentException( + "The parameter '" + parameter + "' is malformed"); + } + } + + private String decode(String encoded) { + try { + return URLDecoder.decode(encoded, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException( + "Unable to URL encode " + encoded + " using UTF-8", ex); + } + + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java similarity index 94% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java index 3f30bd89e..21684d564 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/QueryStringParserTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryStringParserTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.restdocs.cli; +package org.springframework.restdocs.operation; import java.net.URI; import java.util.Arrays; @@ -23,8 +23,6 @@ import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.restdocs.operation.Parameters; - import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasEntry; From 21567e0ffa10a330951727edca62f283ca7f8803 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 22 Aug 2016 15:45:26 +0100 Subject: [PATCH 177/898] Simply dealing with operation names in REST Docs' own tests --- .../restdocs/AbstractSnippetTests.java | 9 +- .../restdocs/cli/CurlRequestSnippetTests.java | 272 +++++++----------- .../cli/HttpieRequestSnippetTests.java | 272 +++++++----------- .../RequestHeadersSnippetFailureTests.java | 20 +- .../headers/RequestHeadersSnippetTests.java | 99 ++++--- .../ResponseHeadersSnippetFailureTests.java | 16 +- .../headers/ResponseHeadersSnippetTests.java | 59 ++-- .../http/HttpRequestSnippetTests.java | 202 ++++++------- .../http/HttpResponseSnippetTests.java | 42 ++- .../hypermedia/LinksSnippetFailureTests.java | 21 +- .../hypermedia/LinksSnippetTests.java | 68 ++--- .../AsciidoctorRequestFieldsSnippetTests.java | 32 ++- .../RequestFieldsSnippetFailureTests.java | 87 +++--- .../payload/RequestFieldsSnippetTests.java | 108 +++---- .../ResponseFieldsSnippetFailureTests.java | 84 +++--- .../payload/ResponseFieldsSnippetTests.java | 136 +++++---- .../PathParametersSnippetFailureTests.java | 37 ++- .../request/PathParametersSnippetTests.java | 116 ++++---- .../RequestParametersSnippetFailureTests.java | 26 +- .../RequestParametersSnippetTests.java | 109 ++++--- .../RequestPartsSnippetFailureTests.java | 21 +- .../request/RequestPartsSnippetTests.java | 98 +++---- .../restdocs/test/ExpectedSnippet.java | 51 ++-- .../restdocs/test/OperationBuilder.java | 55 +++- 24 files changed, 917 insertions(+), 1123 deletions(-) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index 0adc3d0b5..41b18ffda 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -52,6 +52,9 @@ public abstract class AbstractSnippetTests { @Rule public ExpectedSnippet snippet; + @Rule + public OperationBuilder operationBuilder; + @Parameters(name = "{0}") public static List parameters() { return Arrays.asList(new Object[] { "Asciidoctor", asciidoctor() }, @@ -61,6 +64,7 @@ public static List parameters() { public AbstractSnippetTests(String name, TemplateFormat templateFormat) { this.snippet = new ExpectedSnippet(templateFormat); this.templateFormat = templateFormat; + this.operationBuilder = new OperationBuilder(this.templateFormat); } public CodeBlockMatcher codeBlock(String language) { @@ -84,11 +88,6 @@ public HttpResponseMatcher httpResponse(HttpStatus responseStatus) { return SnippetMatchers.httpResponse(this.templateFormat, responseStatus); } - public OperationBuilder operationBuilder(String name) { - return new OperationBuilder(name, this.snippet.getOutputDirectory(), - this.templateFormat); - } - protected FileSystemResource snippetResource(String name) { return new FileSystemResource("src/test/resources/custom-snippet-templates/" + this.templateFormat.getId() + "/" + name + ".snippet"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index 314234b85..c9e2ead72 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -55,245 +55,197 @@ public CurlRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectCurlRequest("get-request").withContents( + this.snippet.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); - new CurlRequestSnippet().document( - operationBuilder("get-request").request("http://localhost/foo").build()); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo").build()); } @Test public void getRequestWithParameter() throws IOException { - this.snippet.expectCurlRequest("get-request").withContents( + this.snippet.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo?a=alpha' -i")); - new CurlRequestSnippet().document(operationBuilder("get-request") + new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").param("a", "alpha").build()); } @Test public void nonGetRequest() throws IOException { - this.snippet.expectCurlRequest("non-get-request").withContents( + this.snippet.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); - new CurlRequestSnippet().document(operationBuilder("non-get-request") + new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").build()); } @Test public void requestWithContent() throws IOException { - this.snippet.expectCurlRequest("request-with-content") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -d 'content'")); - new CurlRequestSnippet().document(operationBuilder("request-with-content") + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -d 'content'")); + new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").content("content").build()); } @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest("request-with-query-string") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document(operationBuilder("request-with-query-string") + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i")); + new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").build()); } @Test public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "request-with-totally-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param=value' -i")); - new CurlRequestSnippet().document(operationBuilder( - "request-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?param=value") + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i")); + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?param=value") .param("param", "value").build()); } @Test public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); - new CurlRequestSnippet().document(operationBuilder( - "request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("a", "alpha") - .param("b", "bravo").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .param("a", "alpha").param("b", "bravo").build()); } @Test public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); - new CurlRequestSnippet().document(operationBuilder( - "request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("b", "bravo") - .build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); } @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest("request-with-query-string-with-no-value") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param' -i")); - new CurlRequestSnippet() - .document(operationBuilder("request-with-query-string-with-no-value") - .request("http://localhost/foo?param").build()); + this.snippet.expectCurlRequest().withContents( + codeBlock("bash").content("$ curl 'http://localhost/foo?param' -i")); + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?param").build()); } @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest("post-request-with-query-string") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); - new CurlRequestSnippet() - .document(operationBuilder("post-request-with-query-string") - .request("http://localhost/foo?param=value").method("POST") - .build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?param=value").method("POST").build()); } @Test public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest("post-request-with-query-string-with-no-value") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo?param' -i -X POST")); - new CurlRequestSnippet() - .document(operationBuilder("post-request-with-query-string-with-no-value") - .request("http://localhost/foo?param").method("POST").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?param' -i -X POST")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?param").method("POST").build()); } @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest("post-request-with-one-parameter") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); new CurlRequestSnippet() - .document(operationBuilder("post-request-with-one-parameter") - .request("http://localhost/foo").method("POST").param("k1", "v1") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("k1", "v1").build()); } @Test public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectCurlRequest("post-request-with-one-parameter-with-no-value") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); - new CurlRequestSnippet().document( - operationBuilder("post-request-with-one-parameter-with-no-value") - .request("http://localhost/foo").method("POST").param("k1") - .build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("POST").param("k1").build()); } @Test public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest("post-request-with-multiple-parameters") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet() - .document(operationBuilder("post-request-with-multiple-parameters") - .request("http://localhost/foo").method("POST") + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo").method("POST") .param("k1", "v1", "v1-bis").param("k2", "v2").build()); } @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest("post-request-with-url-encoded-parameter") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); new CurlRequestSnippet() - .document(operationBuilder("post-request-with-url-encoded-parameter") - .request("http://localhost/foo").method("POST").param("k1", "a&b") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("k1", "a&b").build()); } @Test public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet - .expectCurlRequest( - "post-request-with-disjoint-query-string-and-parameter") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document( - operationBuilder("post-request-with-disjoint-query-string-and-parameter") - .request("http://localhost/foo?a=alpha").method("POST") - .param("b", "bravo").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .method("POST").param("b", "bravo").build()); } @Test public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "post-request-with-totally-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); - new CurlRequestSnippet().document(operationBuilder( - "post-request-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha&b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectCurlRequest( - "post-request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); - new CurlRequestSnippet().document(operationBuilder( - "post-request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").method("POST") - .param("a", "alpha").param("b", "bravo").build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest("put-request-with-one-parameter") - .withContents(codeBlock("bash") - .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); - new CurlRequestSnippet() - .document(operationBuilder("put-request-with-one-parameter") - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .build()); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); } @Test public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest("put-request-with-multiple-parameters") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); - new CurlRequestSnippet() - .document(operationBuilder("put-request-with-multiple-parameters") - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); } @Test public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest("put-request-with-url-encoded-parameter") - .withContents(codeBlock("bash").content( - "$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); + this.snippet.expectCurlRequest().withContents(codeBlock("bash") + .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); new CurlRequestSnippet() - .document(operationBuilder("put-request-with-url-encoded-parameter") - .request("http://localhost/foo").method("PUT").param("k1", "a&b") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("PUT").param("k1", "a&b").build()); } @Test public void requestWithHeaders() throws IOException { - this.snippet.expectCurlRequest("request-with-headers") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet().document( - operationBuilder("request-with-headers").request("http://localhost/foo") + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); @@ -304,15 +256,12 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'metadata={\"description\": \"foo\"}'"; - this.snippet.expectCurlRequest("multipart-post-no-original-filename") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet() - .document(operationBuilder("multipart-post-no-original-filename") - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()) - .build()); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @Test @@ -320,11 +269,10 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png;type=image/png'"; - this.snippet.expectCurlRequest("multipart-post-with-content-type") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet() - .document(operationBuilder("multipart-post-with-content-type") - .request("http://localhost/upload").method("POST") + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -337,11 +285,10 @@ public void multipartPost() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png'"; - this.snippet.expectCurlRequest("multipart-post") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet() - .document(operationBuilder("multipart-post") - .request("http://localhost/upload").method("POST") + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -354,11 +301,10 @@ public void multipartPostWithParameters() throws IOException { + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " + "-F 'b=banana'"; - this.snippet.expectCurlRequest("multipart-post-with-parameters") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet() - .document(operationBuilder("multipart-post-with-parameters") - .request("http://localhost/upload").method("POST") + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -368,10 +314,10 @@ public void multipartPostWithParameters() throws IOException { @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { - this.snippet.expectCurlRequest("basic-auth").withContents(codeBlock("bash") + this.snippet.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -u 'user:secret'")); new CurlRequestSnippet() - .document(operationBuilder("basic-auth").request("http://localhost/foo") + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils .encodeToString("user:secret".getBytes())) @@ -380,7 +326,7 @@ public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException @Test public void customAttributes() throws IOException { - this.snippet.expectCurlRequest("custom-attributes") + this.snippet.expectCurlRequest() .withContents(containsString("curl request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("curl-request")) @@ -389,7 +335,7 @@ public void customAttributes() throws IOException { attributes( key("title").value("curl request title"))) .document( - operationBuilder("custom-attributes") + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -398,12 +344,12 @@ public void customAttributes() throws IOException { @Test public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectCurlRequest("custom-host-header") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Host: api.example.com'" + " -H 'Content-Type: application/json' -H 'a: alpha'")); - new CurlRequestSnippet().document( - operationBuilder("custom-host-header").request("http://localhost/foo") + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) @@ -412,15 +358,13 @@ public void customHostHeaderIsIncluded() throws IOException { @Test public void postWithContentAndParameters() throws IOException { - this.snippet.expectCurlRequest("post-with-content-and-parameters") + this.snippet.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + "-X POST -d 'Some content'")); - new CurlRequestSnippet() - .document(operationBuilder("post-with-content-and-parameters") - .request("http://localhost/foo").param("a", "alpha") - .method("POST").param("b", "bravo").content("Some content") - .build()); + new CurlRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").param("a", "alpha").method("POST") + .param("b", "bravo").content("Some content").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 9dd297c6f..527a2c7f8 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -56,245 +56,197 @@ public HttpieRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectHttpieRequest("get-request").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo'")); - new HttpieRequestSnippet().document( - operationBuilder("get-request").request("http://localhost/foo").build()); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo").build()); } @Test public void getRequestWithParameter() throws IOException { - this.snippet.expectHttpieRequest("get-request-with-parameter").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo?a=alpha'")); - new HttpieRequestSnippet().document(operationBuilder("get-request-with-parameter") + new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").param("a", "alpha").build()); } @Test public void nonGetRequest() throws IOException { - this.snippet.expectHttpieRequest("non-get-request").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ http POST 'http://localhost/foo'")); - new HttpieRequestSnippet().document(operationBuilder("non-get-request") + new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").build()); } @Test public void requestWithContent() throws IOException { - this.snippet.expectHttpieRequest("request-with-content") - .withContents(codeBlock("bash") - .content("$ echo 'content' | http GET 'http://localhost/foo'")); - new HttpieRequestSnippet().document(operationBuilder("request-with-content") + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ echo 'content' | http GET 'http://localhost/foo'")); + new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").content("content").build()); } @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest("request-with-query-string") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?param=value'")); - new HttpieRequestSnippet().document(operationBuilder("request-with-query-string") + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").build()); } @Test public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "request-with-totally-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?param=value'")); - new HttpieRequestSnippet().document(operationBuilder( - "request-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?param=value") + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?param=value") .param("param", "value").build()); } @Test public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet().document(operationBuilder( - "request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("a", "alpha") - .param("b", "bravo").build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .param("a", "alpha").param("b", "bravo").build()); } @Test public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet().document(operationBuilder( - "request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("b", "bravo") - .build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); } @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest("request-with-query-string-with-no-value") - .withContents(codeBlock("bash") - .content("$ http GET 'http://localhost/foo?param'")); - new HttpieRequestSnippet() - .document(operationBuilder("request-with-query-string-with-no-value") - .request("http://localhost/foo?param").build()); + this.snippet.expectHttpieRequest().withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo?param'")); + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?param").build()); } @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-query-string") - .withContents(codeBlock("bash") - .content("$ http POST 'http://localhost/foo?param=value'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-query-string") - .request("http://localhost/foo?param=value").method("POST") - .build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?param=value'")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?param=value").method("POST").build()); } @Test public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-query-string-with-no-value") - .withContents(codeBlock("bash") - .content("$ http POST 'http://localhost/foo?param'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-query-string-with-no-value") - .request("http://localhost/foo?param").method("POST").build()); + this.snippet.expectHttpieRequest().withContents( + codeBlock("bash").content("$ http POST 'http://localhost/foo?param'")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?param").method("POST").build()); } @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-one-parameter") - .withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-one-parameter") - .request("http://localhost/foo").method("POST").param("k1", "v1") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("k1", "v1").build()); } @Test public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-one-parameter-with-no-value") - .withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo' 'k1='")); - new HttpieRequestSnippet().document( - operationBuilder("post-request-with-one-parameter-with-no-value") - .request("http://localhost/foo").method("POST").param("k1") - .build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1='")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("POST").param("k1").build()); } @Test public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-multiple-parameters") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-multiple-parameters") - .request("http://localhost/foo").method("POST") + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo").method("POST") .param("k1", "v1", "v1-bis").param("k2", "v2").build()); } @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest("post-request-with-url-encoded-parameter") - .withContents(codeBlock("bash") - .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() - .document(operationBuilder("post-request-with-url-encoded-parameter") - .request("http://localhost/foo").method("POST").param("k1", "a&b") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("k1", "a&b").build()); } @Test public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet - .expectHttpieRequest( - "post-request-with-disjoint-query-string-and-parameter") - .withContents(codeBlock("bash").content( - "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - new HttpieRequestSnippet().document( - operationBuilder("post-request-with-disjoint-query-string-and-parameter") - .request("http://localhost/foo?a=alpha").method("POST") - .param("b", "bravo").build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .method("POST").param("b", "bravo").build()); } @Test public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "post-request-with-totally-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash") - .content("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet().document(operationBuilder( - "post-request-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha&b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpieRequest( - "post-request-with-partially-overlapping-query-string-and-parameters") - .withContents(codeBlock("bash").content( - "$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); - new HttpieRequestSnippet().document(operationBuilder( - "post-request-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").method("POST") - .param("a", "alpha").param("b", "bravo").build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); } @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest("put-request-with-one-parameter") - .withContents(codeBlock("bash") - .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); - new HttpieRequestSnippet() - .document(operationBuilder("put-request-with-one-parameter") - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .build()); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); } @Test public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest("put-request-with-multiple-parameters") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash") .content("$ http --form PUT 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); - new HttpieRequestSnippet() - .document(operationBuilder("put-request-with-multiple-parameters") - .request("http://localhost/foo").method("PUT").param("k1", "v1") - .param("k1", "v1-bis").param("k2", "v2").build()); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("PUT").param("k1", "v1") + .param("k1", "v1-bis").param("k2", "v2").build()); } @Test public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest("put-request-with-url-encoded-parameter") - .withContents(codeBlock("bash") - .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() - .document(operationBuilder("put-request-with-url-encoded-parameter") - .request("http://localhost/foo").method("PUT").param("k1", "a&b") - .build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("PUT").param("k1", "a&b").build()); } @Test public void requestWithHeaders() throws IOException { - this.snippet.expectHttpieRequest("request-with-headers").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo'" + " 'Content-Type:application/json' 'a:alpha'")); - new HttpieRequestSnippet().document( - operationBuilder("request-with-headers").request("http://localhost/foo") + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); @@ -305,15 +257,12 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'metadata'@<(echo '{\"description\": \"foo\"}')"); - this.snippet.expectHttpieRequest("multipart-post-no-original-filename") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet() - .document(operationBuilder("multipart-post-no-original-filename") - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.MULTIPART_FORM_DATA_VALUE) - .part("metadata", "{\"description\": \"foo\"}".getBytes()) - .build()); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @Test @@ -322,11 +271,10 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest("multipart-post-with-content-type") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet() - .document(operationBuilder("multipart-post-with-content-type") - .request("http://localhost/upload").method("POST") + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -339,11 +287,10 @@ public void multipartPost() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest("multipart-post") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet() - .document(operationBuilder("multipart-post") - .request("http://localhost/upload").method("POST") + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -356,11 +303,10 @@ public void multipartPostWithParameters() throws IOException { .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado'" + " 'b=banana'"); - this.snippet.expectHttpieRequest("multipart-post-with-parameters") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet() - .document(operationBuilder("multipart-post-with-parameters") - .request("http://localhost/upload").method("POST") + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", new byte[0]) @@ -370,10 +316,10 @@ public void multipartPostWithParameters() throws IOException { @Test public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { - this.snippet.expectHttpieRequest("basic-auth").withContents(codeBlock("bash") + this.snippet.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); new HttpieRequestSnippet() - .document(operationBuilder("basic-auth").request("http://localhost/foo") + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils .encodeToString("user:secret".getBytes())) @@ -382,7 +328,7 @@ public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException @Test public void customAttributes() throws IOException { - this.snippet.expectHttpieRequest("custom-attributes") + this.snippet.expectHttpieRequest() .withContents(containsString("httpie request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("httpie-request")) @@ -391,7 +337,7 @@ public void customAttributes() throws IOException { attributes( key("title").value("httpie request title"))) .document( - operationBuilder("custom-attributes") + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -400,12 +346,12 @@ public void customAttributes() throws IOException { @Test public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectHttpieRequest("custom-host-header") + this.snippet.expectHttpieRequest() .withContents(codeBlock("bash").content( "$ http GET 'http://localhost/foo' 'Host:api.example.com'" + " 'Content-Type:application/json' 'a:alpha'")); - new HttpieRequestSnippet().document( - operationBuilder("custom-host-header").request("http://localhost/foo") + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") .header(HttpHeaders.HOST, "api.example.com") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) @@ -414,14 +360,12 @@ public void customHostHeaderIsIncluded() throws IOException { @Test public void postWithContentAndParameters() throws IOException { - this.snippet.expectHttpieRequest("post-with-content-and-parameters").withContents( + this.snippet.expectHttpieRequest().withContents( codeBlock("bash").content("$ echo 'Some content' | http POST " + "'http://localhost/foo?a=alpha&b=bravo'")); - new HttpieRequestSnippet() - .document(operationBuilder("post-with-content-and-parameters") - .request("http://localhost/foo").method("POST") - .param("a", "alpha").param("b", "bravo").content("Some content") - .build()); + new HttpieRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo").method("POST").param("a", "alpha") + .param("b", "bravo").content("Some content").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java index 33ccabb27..f1192567c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -24,13 +24,13 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link RequestHeadersSnippet} due to missing or @@ -41,7 +41,10 @@ public class RequestHeadersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -53,10 +56,8 @@ public void missingRequestHeader() throws IOException { .expectMessage(equalTo("Headers with the following names were not found" + " in the request: [Accept]")); new RequestHeadersSnippet( - Arrays.asList(headerWithName("Accept").description("one"))) - .document(new OperationBuilder("missing-request-headers", - this.snippet.getOutputDirectory()) - .request("http://localhost").build()); + Arrays.asList(headerWithName("Accept").description("one"))).document( + this.operationBuilder.request("http://localhost").build()); } @Test @@ -67,11 +68,8 @@ public void undocumentedRequestHeaderAndMissingRequestHeader() throws IOExceptio + " in the request: [Accept]")); new RequestHeadersSnippet( Arrays.asList(headerWithName("Accept").description("one"))) - .document(new OperationBuilder( - "undocumented-request-header-and-missing-request-header", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .header("X-Test", "test").build()); + .document(this.operationBuilder.request("http://localhost") + .header("X-Test", "test").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 5766661c5..805c1f7cb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -27,7 +27,6 @@ import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; @@ -50,7 +49,7 @@ public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectRequestHeaders("request-with-headers") + this.snippet.expectRequestHeaders() .withContents(tableWithHeader("Name", "Description") .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") @@ -62,64 +61,71 @@ public void requestWithHeaders() throws IOException { headerWithName("Accept-Encoding").description("three"), headerWithName("Accept-Language").description("four"), headerWithName("Cache-Control").description("five"), - headerWithName("Connection").description("six"))) - .document(operationBuilder("request-with-headers") - .request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .header("Accept-Encoding", "gzip, deflate") - .header("Accept-Language", "en-US,en;q=0.5") - .header("Cache-Control", "max-age=0") - .header("Connection", "keep-alive").build()); + headerWithName( + "Connection") + .description("six"))) + .document( + this.operationBuilder + .request( + "http://localhost") + .header("X-Test", "test") + .header("Accept", "*/*") + .header("Accept-Encoding", + "gzip, deflate") + .header("Accept-Language", + "en-US,en;q=0.5") + .header("Cache-Control", + "max-age=0") + .header("Connection", + "keep-alive") + .build()); } @Test public void caseInsensitiveRequestHeaders() throws IOException { - this.snippet.expectRequestHeaders("case-insensitive-request-headers") - .withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one")); + this.snippet.expectRequestHeaders().withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) - .document(operationBuilder("case-insensitive-request-headers") - .request("/").header("X-test", "test").build()); + .document(this.operationBuilder.request("/") + .header("X-test", "test").build()); } @Test public void undocumentedRequestHeader() throws IOException { new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) - .document(new OperationBuilder("undocumented-request-header", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .header("X-Test", "test").header("Accept", "*/*") - .build()); + .document(this.operationBuilder.request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") + .build()); } @Test public void requestHeadersWithCustomAttributes() throws IOException { - this.snippet.expectRequestHeaders("request-headers-with-custom-attributes") - .withContents(containsString("Custom title")); + this.snippet.expectRequestHeaders().withContents(containsString("Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")) .willReturn(snippetResource("request-headers-with-title")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one")), - attributes(key("title").value("Custom title"))).document( - operationBuilder("request-headers-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").header("X-Test", "test") - .build()); + attributes( + key("title").value("Custom title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .header("X-Test", "test").build()); } @Test public void requestHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet - .expectRequestHeaders("request-headers-with-custom-descriptor-attributes") - .withContents(// - tableWithHeader("Name", "Description", "Foo") - .row("X-Test", "one", "alpha") - .row("Accept-Encoding", "two", "bravo") - .row("Accept", "three", "charlie")); + this.snippet.expectRequestHeaders().withContents(// + tableWithHeader("Name", "Description", "Foo") + .row("X-Test", "one", "alpha") + .row("Accept-Encoding", "two", "bravo") + .row("Accept", "three", "charlie")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")) .willReturn(snippetResource("request-headers-with-extra-column")); @@ -130,8 +136,8 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("bravo")), headerWithName("Accept").description("three") .attributes(key("foo").value("charlie")))) - .document(operationBuilder( - "request-headers-with-custom-descriptor-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -144,7 +150,7 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestHeaders("additional-descriptors") + this.snippet.expectRequestHeaders() .withContents(tableWithHeader("Name", "Description") .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") @@ -156,10 +162,12 @@ public void additionalDescriptors() throws IOException { headerWithName("Accept-Encoding").description("three"), headerWithName("Accept-Language").description("four")) .and(headerWithName("Cache-Control").description("five"), - headerWithName("Connection").description("six")) - .document(operationBuilder("additional-descriptors") - .request("http://localhost").header("X-Test", "test") - .header("Accept", "*/*") + headerWithName( + "Connection") + .description( + "six")) + .document(this.operationBuilder.request("http://localhost") + .header("X-Test", "test").header("Accept", "*/*") .header("Accept-Encoding", "gzip, deflate") .header("Accept-Language", "en-US,en;q=0.5") .header("Cache-Control", "max-age=0") @@ -168,14 +176,13 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectRequestHeaders("request-with-escaped-headers").withContents( + this.snippet.expectRequestHeaders().withContents( tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new RequestHeadersSnippet( Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) - .document(operationBuilder("request-with-escaped-headers") - .request("http://localhost").header("Foo|Bar", "baz") - .build()); + .document(this.operationBuilder.request("http://localhost") + .header("Foo|Bar", "baz").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java index 997a3bc8a..e42d38179 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -24,13 +24,13 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link ResponseHeadersSnippet} due to missing or @@ -41,7 +41,10 @@ public class ResponseHeadersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -54,8 +57,7 @@ public void missingResponseHeader() throws IOException { + " in the response: [Content-Type]")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("Content-Type").description("one"))) - .document(new OperationBuilder("missing-response-headers", - this.snippet.getOutputDirectory()).response().build()); + .document(this.operationBuilder.response().build()); } @Test @@ -66,10 +68,8 @@ public void undocumentedResponseHeaderAndMissingResponseHeader() throws IOExcept + " in the response: [Content-Type]")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("Content-Type").description("one"))) - .document(new OperationBuilder( - "undocumented-response-header-and-missing-response-header", - this.snippet.getOutputDirectory()).response() - .header("X-Test", "test").build()); + .document(this.operationBuilder.response() + .header("X-Test", "test").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 417259cba..86ac3eef6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -27,7 +27,6 @@ import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.TemplateResourceResolver; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; -import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; @@ -50,7 +49,7 @@ public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders("response-headers").withContents( + this.snippet.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one") .row("`Content-Type`", "two").row("`Etag`", "three") .row("`Cache-Control`", "five").row("`Vary`", "six")); @@ -61,7 +60,7 @@ public void responseWithHeaders() throws IOException { headerWithName("Cache-Control").description("five"), headerWithName("Vary").description("six"))) .document( - operationBuilder("response-headers").response() + this.operationBuilder.response() .header("X-Test", "test") .header("Content-Type", "application/json") @@ -72,44 +71,44 @@ public void responseWithHeaders() throws IOException { @Test public void caseInsensitiveResponseHeaders() throws IOException { - this.snippet.expectResponseHeaders("case-insensitive-response-headers") - .withContents( - tableWithHeader("Name", "Description").row("`X-Test`", "one")); + this.snippet.expectResponseHeaders().withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) - .document(operationBuilder("case-insensitive-response-headers") - .response().header("X-test", "test").build()); + .document(this.operationBuilder.response() + .header("X-test", "test").build()); } @Test public void undocumentedResponseHeader() throws IOException { new ResponseHeadersSnippet( - Arrays.asList(headerWithName("X-Test").description("one"))) - .document(new OperationBuilder("undocumented-response-header", - this.snippet.getOutputDirectory()).response() - .header("X-Test", "test") - .header("Content-Type", "*/*").build()); + Arrays.asList(headerWithName("X-Test").description("one"))).document( + this.operationBuilder.response().header("X-Test", "test") + .header("Content-Type", "*/*").build()); } @Test public void responseHeadersWithCustomAttributes() throws IOException { - this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") - .withContents(containsString("Custom title")); + this.snippet.expectResponseHeaders().withContents(containsString("Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-headers")) .willReturn(snippetResource("response-headers-with-title")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one")), - attributes(key("title").value("Custom title"))).document( - operationBuilder("response-headers-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .response().header("X-Test", "test").build()); + attributes( + key("title").value("Custom title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response().header("X-Test", "test") + .build()); } @Test public void responseHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectResponseHeaders("response-headers-with-custom-attributes") + this.snippet.expectResponseHeaders() .withContents(tableWithHeader("Name", "Description", "Foo") .row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo") .row("Etag", "three", "charlie")); @@ -123,8 +122,8 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("bravo")), headerWithName("Etag").description("three") .attributes(key("foo").value("charlie")))) - .document(operationBuilder( - "response-headers-with-custom-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -137,7 +136,7 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseHeaders("additional-descriptors").withContents( + this.snippet.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one") .row("`Content-Type`", "two").row("`Etag`", "three") .row("`Cache-Control`", "five").row("`Vary`", "six")); @@ -146,10 +145,8 @@ public void additionalDescriptors() throws IOException { headerWithName("Content-Type").description("two"), headerWithName("Etag").description("three")) .and(headerWithName("Cache-Control").description("five"), - headerWithName("Vary") - .description("six")) - .document(operationBuilder("additional-descriptors").response() - .header("X-Test", "test") + headerWithName("Vary").description("six")) + .document(this.operationBuilder.response().header("X-Test", "test") .header("Content-Type", "application/json") .header("Etag", "lskjadldj3ii32l2ij23") .header("Cache-Control", "max-age=0").header("Vary", "User-Agent") @@ -158,13 +155,13 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectResponseHeaders("response-with-escaped-headers").withContents( + this.snippet.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new ResponseHeadersSnippet( Arrays.asList(headerWithName("Foo|Bar").description("one|two"))) - .document(operationBuilder("response-with-escaped-headers") - .response().header("Foo|Bar", "baz").build()); + .document(this.operationBuilder.response() + .header("Foo|Bar", "baz").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index e66e6b5be..6ba4d65a3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -51,245 +51,222 @@ public HttpRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectHttpRequest("get-request") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(operationBuilder("get-request") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").header("Alpha", "a").build()); } @Test public void getRequestWithParameters() throws IOException { - this.snippet.expectHttpRequest("get-request-with-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?b=bravo") .header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(operationBuilder("get-request-with-parameters") - .request("http://localhost/foo").header("Alpha", "a").param("b", "bravo") - .build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .header("Alpha", "a").param("b", "bravo").build()); } @Test public void getRequestWithPort() throws IOException { - this.snippet.expectHttpRequest("get-request") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") .header(HttpHeaders.HOST, "localhost:8080")); - new HttpRequestSnippet().document(operationBuilder("get-request") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost:8080/foo").header("Alpha", "a").build()); } @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpRequest("get-request-with-query-string") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?bar=baz") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet() - .document(operationBuilder("get-request-with-query-string") - .request("http://localhost/foo?bar=baz").build()); + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?bar=baz").build()); } @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpRequest("get-request-with-query-string-with-no-value") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?bar") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet() - .document(operationBuilder("get-request-with-query-string-with-no-value") - .request("http://localhost/foo?bar").build()); + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?bar").build()); } @Test public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpRequest( - "get-with-partially-overlapping-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(operationBuilder( - "get-with-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha").param("a", "alpha") - .param("b", "bravo").build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?a=alpha") + .param("a", "alpha").param("b", "bravo").build()); } @Test public void getWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet - .expectHttpRequest( - "get-with-totally-overlapping-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") .header(HttpHeaders.HOST, "localhost")); - new HttpRequestSnippet().document(operationBuilder( - "get-with-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?a=alpha&b=bravo") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") .param("a", "alpha").param("b", "bravo").build()); } @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("post-request-with-content") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder("post-request-with-content") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").content(content).build()); } @Test public void postRequestWithContentAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("post-request-with-content-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet() - .document(operationBuilder("post-request-with-content-and-parameters") - .request("http://localhost/foo").method("POST") - .param("a", "alpha").content(content).build()); + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("a", "alpha").content(content).build()); } @Test public void postRequestWithContentAndDisjointQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet - .expectHttpRequest( - "post-request-with-content-and-disjoint-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder( - "post-request-with-content-and-disjoint-query-string-and-parameters") - .request("http://localhost/foo?b=bravo").method("POST") - .param("a", "alpha").content(content).build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo?b=bravo") + .method("POST").param("a", "alpha").content(content).build()); } @Test public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet - .expectHttpRequest( - "post-request-with-content-and-partially-overlapping-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder( - "post-request-with-content-and-partially-overlapping-query-string-and-parameters") - .request("http://localhost/foo?b=bravo").method("POST") - .param("a", "alpha").param("b", "bravo").content(content) - .build()); + new HttpRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?b=bravo").method("POST") + .param("a", "alpha").param("b", "bravo").content(content).build()); } @Test public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet - .expectHttpRequest( - "post-request-with-content-and-totally-overlapping-query-string-and-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content) - .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content).header( + HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder( - "post-request-with-content-and-totally-overlapping-query-string-and-parameters") - .request("http://localhost/foo?b=bravo&a=alpha").method("POST") - .param("a", "alpha").param("b", "bravo").content(content) - .build()); + new HttpRequestSnippet().document(this.operationBuilder + .request("http://localhost/foo?b=bravo&a=alpha").method("POST") + .param("a", "alpha").param("b", "bravo").content(content).build()); } @Test public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpRequest("post-request-with-charset") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header("Content-Type", "text/plain;charset=UTF-8") .header(HttpHeaders.HOST, "localhost") .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length) .content(japaneseContent)); - new HttpRequestSnippet().document(operationBuilder("post-request-with-charset") - .request("http://localhost/foo").method("POST") - .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) - .build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").header("Content-Type", "text/plain;charset=UTF-8") + .content(contentBytes).build()); } @Test public void postRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest("post-request-with-parameter") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(operationBuilder("post-request-with-parameter") - .request("http://localhost/foo").method("POST").param("b&r", "baz") - .param("a", "alpha").build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .method("POST").param("b&r", "baz").param("a", "alpha").build()); } @Test public void postRequestWithParameterWithNoValue() throws IOException { - this.snippet.expectHttpRequest("post-request-with-parameter") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("bar=")); - new HttpRequestSnippet().document(operationBuilder("post-request-with-parameter") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").param("bar").build()); } @Test public void putRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest("put-request-with-content") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpRequestSnippet().document(operationBuilder("put-request-with-content") + new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("PUT").content(content).build()); } @Test public void putRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest("put-request-with-parameter") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") .content("b%26r=baz&a=alpha")); - new HttpRequestSnippet().document(operationBuilder("put-request-with-parameter") - .request("http://localhost/foo").method("PUT").param("b&r", "baz") - .param("a", "alpha").build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .method("PUT").param("b&r", "baz").param("a", "alpha").build()); } @Test public void multipartPost() throws IOException { String expectedContent = createPart(String.format( "Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); - this.snippet - .expectHttpRequest( - "multipart-post") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet() - .document(operationBuilder("multipart-post") - .request("http://localhost/upload").method("POST") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()).build()); @@ -309,16 +286,13 @@ public void multipartPostWithParameters() throws IOException { String filePart = createPart(String .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = param1Part + param2Part + param3Part + filePart; - this.snippet - .expectHttpRequest( - "multipart-post-with-parameters") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet() - .document(operationBuilder("multipart-post-with-parameters") - .request("http://localhost/upload").method("POST") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .param("a", "apple", "avocado").param("b", "banana") @@ -332,16 +306,13 @@ public void multipartPostWithParameterWithNoValue() throws IOException { String filePart = createPart(String .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = paramPart + filePart; - this.snippet - .expectHttpRequest( - "multipart-post-with-parameter-with-no-value") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet() - .document(operationBuilder("multipart-post-with-parameter-with-no-value") - .request("http://localhost/upload").method("POST") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .param("a").part("image", "<< data >>".getBytes()).build()); @@ -352,16 +323,13 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = createPart( String.format("Content-Disposition: form-data; name=image%nContent-Type: " + "image/png%n%n<< data >>")); - this.snippet - .expectHttpRequest( - "multipart-post-with-content-type") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) .header(HttpHeaders.HOST, "localhost").content(expectedContent)); - new HttpRequestSnippet() - .document(operationBuilder("multipart-post-with-content-type") - .request("http://localhost/upload").method("POST") + new HttpRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) .part("image", "<< data >>".getBytes()) @@ -371,26 +339,30 @@ public void multipartPostWithContentType() throws IOException { @Test public void getRequestWithCustomHost() throws IOException { - this.snippet.expectHttpRequest("get-request-custom-host") + this.snippet.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo") .header(HttpHeaders.HOST, "api.example.com")); - new HttpRequestSnippet().document(operationBuilder("get-request-custom-host") - .request("http://localhost/foo") - .header(HttpHeaders.HOST, "api.example.com").build()); + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .header(HttpHeaders.HOST, "api.example.com").build()); } @Test public void requestWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpRequest("request-with-snippet-attributes") + this.snippet.expectHttpRequest() .withContents(containsString("Title for the request")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-request")) .willReturn(snippetResource("http-request-with-title")); - new HttpRequestSnippet(attributes(key("title").value("Title for the request"))) - .document(operationBuilder("request-with-snippet-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost/foo").build()); + new HttpRequestSnippet( + attributes( + key("title").value("Title for the request"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost/foo").build()); } private String createPart(String content) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 120812eea..8560fd399 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -49,26 +49,24 @@ public HttpResponseSnippetTests(String name, TemplateFormat templateFormat) { @Test public void basicResponse() throws IOException { - this.snippet.expectHttpResponse("basic-response") - .withContents(httpResponse(HttpStatus.OK)); - new HttpResponseSnippet().document(operationBuilder("basic-response").build()); + this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK)); + new HttpResponseSnippet().document(this.operationBuilder.build()); } @Test public void nonOkResponse() throws IOException { - this.snippet.expectHttpResponse("non-ok-response") + this.snippet.expectHttpResponse() .withContents(httpResponse(HttpStatus.BAD_REQUEST)); - new HttpResponseSnippet().document(operationBuilder("non-ok-response").response() + new HttpResponseSnippet().document(this.operationBuilder.response() .status(HttpStatus.BAD_REQUEST.value()).build()); } @Test public void responseWithHeaders() throws IOException { - this.snippet.expectHttpResponse("response-with-headers") - .withContents(httpResponse(HttpStatus.OK) - .header("Content-Type", "application/json").header("a", "alpha")); + this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK) + .header("Content-Type", "application/json").header("a", "alpha")); new HttpResponseSnippet() - .document(operationBuilder("response-with-headers").response() + .document(this.operationBuilder.response() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .header("a", "alpha").build()); @@ -77,41 +75,37 @@ public void responseWithHeaders() throws IOException { @Test public void responseWithContent() throws IOException { String content = "content"; - this.snippet.expectHttpResponse("response-with-content") + this.snippet.expectHttpResponse() .withContents(httpResponse(HttpStatus.OK).content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); - new HttpResponseSnippet().document(operationBuilder("response-with-content") - .response().content(content).build()); + new HttpResponseSnippet() + .document(this.operationBuilder.response().content(content).build()); } @Test public void responseWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpResponse("response-with-charset") + this.snippet.expectHttpResponse() .withContents(httpResponse(HttpStatus.OK) .header("Content-Type", "text/plain;charset=UTF-8") .content(japaneseContent) .header(HttpHeaders.CONTENT_LENGTH, contentBytes.length)); - new HttpResponseSnippet().document(operationBuilder("response-with-charset") - .response().header("Content-Type", "text/plain;charset=UTF-8") - .content(contentBytes).build()); + new HttpResponseSnippet().document(this.operationBuilder.response() + .header("Content-Type", "text/plain;charset=UTF-8").content(contentBytes) + .build()); } @Test public void responseWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpResponse("response-with-snippet-attributes") + this.snippet.expectHttpResponse() .withContents(containsString("Title for the response")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-response")) .willReturn(snippetResource("http-response-with-title")); - new HttpResponseSnippet( - attributes(key("title").value("Title for the response"))) - .document( - operationBuilder("response-with-snippet-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .build()); + new HttpResponseSnippet(attributes(key("title").value("Title for the response"))) + .document(this.operationBuilder.attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)).build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index b8f994703..f8e0cf762 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -25,11 +25,11 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link LinksSnippet} due to missing or undocumented @@ -40,7 +40,10 @@ public class LinksSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -52,8 +55,7 @@ public void undocumentedLink() throws IOException { "Links with the following relations were not" + " documented: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), Collections.emptyList()) - .document(new OperationBuilder("undocumented-link", - this.snippet.getOutputDirectory()).build()); + .document(this.operationBuilder.build()); } @Test @@ -63,8 +65,7 @@ public void missingLink() throws IOException { + " found in the response: [foo]")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar"))) - .document(new OperationBuilder("missing-link", - this.snippet.getOutputDirectory()).build()); + .document(this.operationBuilder.build()); } @Test @@ -74,9 +75,8 @@ public void undocumentedLinkAndMissingLink() throws IOException { + " documented: [a]. Links with the following relations were not" + " found in the response: [foo]")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha")), - Arrays.asList(new LinkDescriptor("foo").description("bar"))).document( - new OperationBuilder("undocumented-link-and-missing-link", - this.snippet.getOutputDirectory()).build()); + Arrays.asList(new LinkDescriptor("foo").description("bar"))) + .document(this.operationBuilder.build()); } @Test @@ -87,8 +87,7 @@ public void linkWithNoDescription() throws IOException { + " title was available from the link in the payload")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "bar")), Arrays.asList(new LinkDescriptor("foo"))) - .document(new OperationBuilder("link-with-no-description", - this.snippet.getOutputDirectory()).build()); + .document(this.operationBuilder.build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index eb14248b7..3236bd8ec 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -47,70 +47,66 @@ public LinksSnippetTests(String name, TemplateFormat templateFormat) { @Test public void ignoredLink() throws IOException { - this.snippet.expectLinks("ignored-link").withContents( + this.snippet.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("a").ignored(), new LinkDescriptor("b").description("Link b"))) - .document(operationBuilder("ignored-link").build()); + .document(this.operationBuilder.build()); } @Test public void allUndocumentedLinksCanBeIgnored() throws IOException { - this.snippet.expectLinks("ignore-all-undocumented").withContents( + this.snippet.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), Arrays.asList(new LinkDescriptor("b").description("Link b")), true) - .document(operationBuilder("ignore-all-undocumented").build()); + .document(this.operationBuilder.build()); } @Test public void presentOptionalLink() throws IOException { - this.snippet.expectLinks("present-optional-link").withContents( + this.snippet.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(operationBuilder("present-optional-link").build()); + .document(this.operationBuilder.build()); } @Test public void missingOptionalLink() throws IOException { - this.snippet.expectLinks("missing-optional-link").withContents( + this.snippet.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) - .document(operationBuilder("missing-optional-link").build()); + .document(this.operationBuilder.build()); } @Test public void documentedLinks() throws IOException { - this.snippet.expectLinks("documented-links") - .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") - .row("`b`", "two")); + this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + .row("`a`", "one").row("`b`", "two")); 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(operationBuilder("documented-links").build()); + .document(this.operationBuilder.build()); } @Test public void linkDescriptionFromTitleInPayload() throws IOException { - this.snippet.expectLinks("link-description-from-title-in-payload") - .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") - .row("`b`", "Link b")); + this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + .row("`a`", "one").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), new Link("b", "bravo", "Link b")), Arrays.asList(new LinkDescriptor("a").description("one"), - new LinkDescriptor("b"))).document( - operationBuilder("link-description-from-title-in-payload") - .build()); + new LinkDescriptor("b"))).document(this.operationBuilder.build()); } @Test @@ -118,8 +114,7 @@ public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) .willReturn(snippetResource("links-with-title")); - this.snippet.expectLinks("links-with-custom-attributes") - .withContents(containsString("Title for the links")); + this.snippet.expectLinks().withContents(containsString("Title for the links")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -128,7 +123,7 @@ public void linksWithCustomAttributes() throws IOException { new LinkDescriptor("b").description("two")), attributes(key("title").value("Title for the links"))) .document( - operationBuilder("links-with-custom-attributes") + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(resolver)) .build()); @@ -139,7 +134,7 @@ public void linksWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) .willReturn(snippetResource("links-with-extra-column")); - this.snippet.expectLinks("links-with-custom-descriptor-attributes") + this.snippet.expectLinks() .withContents(tableWithHeader("Relation", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -149,38 +144,33 @@ public void linksWithCustomDescriptorAttributes() throws IOException { Arrays.asList( new LinkDescriptor("a").description("one") .attributes(key("foo").value("alpha")), - new LinkDescriptor("b").description("two").attributes( - key("foo").value("bravo")))) - .document(operationBuilder( - "links-with-custom-descriptor-attributes") - .attribute(TemplateEngine.class - .getName(), - new MustacheTemplateEngine( - resolver)) - .build()); + new LinkDescriptor("b").description("two") + .attributes(key("foo").value("bravo")))) + .document(this.operationBuilder.attribute( + TemplateEngine.class.getName(), + new MustacheTemplateEngine(resolver)) + .build()); } @Test public void additionalDescriptors() throws IOException { - this.snippet.expectLinks("additional-descriptors") - .withContents(tableWithHeader("Relation", "Description").row("`a`", "one") - .row("`b`", "two")); + this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + .row("`a`", "one").row("`b`", "two")); HypermediaDocumentation .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b", "bravo")), new LinkDescriptor("a").description("one")) .and(new LinkDescriptor("b").description("two")) - .document(operationBuilder("additional-descriptors").build()); + .document(this.operationBuilder.build()); } @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectLinks("links-with-escaped-content") - .withContents(tableWithHeader("Relation", "Description").row( - escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")), Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two"))) - .document(operationBuilder("links-with-escaped-content").build()); + .document(this.operationBuilder.build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index 69c32563d..d1a83a662 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -42,6 +42,9 @@ */ public class AsciidoctorRequestFieldsSnippetTests { + @Rule + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + @Rule public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @@ -50,21 +53,22 @@ public void requestFieldsWithListDescription() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-list-description")); - this.snippet.expectRequestFields("request-fields-with-list-description") - .withContents( - tableWithHeader(asciidoctor(), "Path", "Type", "Description") - // - .row("a", "String", String.format(" - one%n - two")) - .configuration("[cols=\"1,1,1a\"]")); + this.snippet.expectRequestFields().withContents( + tableWithHeader(asciidoctor(), "Path", "Type", "Description") + // + .row("a", "String", String.format(" - one%n - two")) + .configuration("[cols=\"1,1,1a\"]")); - new RequestFieldsSnippet(Arrays.asList( - fieldWithPath("a").description(Arrays.asList("one", "two")))).document( - new OperationBuilder("request-fields-with-list-description", - this.snippet.getOutputDirectory()) - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost") - .content("{\"a\": \"foo\"}").build()); + new RequestFieldsSnippet( + Arrays.asList( + fieldWithPath("a").description(Arrays.asList("one", "two")))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } private FileSystemResource snippetResource(String name) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index f27633c2f..30d16443a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -47,6 +47,10 @@ public class RequestFieldsSnippetFailureTests { @Rule public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + @Rule + public OperationBuilder operationBuilder = new OperationBuilder( + TemplateFormats.asciidoctor()); + @Rule public ExpectedException thrown = ExpectedException.none(); @@ -56,9 +60,8 @@ public void undocumentedRequestField() throws IOException { this.thrown.expectMessage(startsWith( "The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{\"a\": 5}").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": 5}").build()); } @Test @@ -67,9 +70,8 @@ public void missingRequestField() throws IOException { 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(new OperationBuilder("missing-request-fields", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{}").build()); + .document(this.operationBuilder.request("http://localhost").content("{}") + .build()); } @Test @@ -77,11 +79,8 @@ public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet( Arrays.asList(fieldWithPath("a.b").description("one").optional())) - .document(new OperationBuilder( - "missing-optional-request-field-with-no-type", - this.snippet.getOutputDirectory()) - .request("http://localhost").content("{ }") - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("{ }").build()); } @Test @@ -93,10 +92,8 @@ public void undocumentedRequestFieldAndMissingRequestField() throws IOException .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(new OperationBuilder( - "undocumented-request-field-and-missing-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("{ \"a\": { \"c\": 5 }}").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{ \"a\": { \"c\": 5 }}").build()); } @Test @@ -105,9 +102,7 @@ public void attemptToDocumentFieldsWithNoRequestBody() throws IOException { this.thrown.expectMessage( equalTo("Cannot document request fields as the request body is empty")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("no-request-body", - this.snippet.getOutputDirectory()).request("http://localhost") - .build()); + .document(this.operationBuilder.request("http://localhost").build()); } @Test @@ -117,10 +112,8 @@ public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException + " Object but the actual type is Number")); new RequestFieldsSnippet(Arrays .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) - .document(new OperationBuilder("mismatched-field-types", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("{ \"a\": 5 }").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{ \"a\": 5 }").build()); } @Test @@ -130,11 +123,8 @@ public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException + " Object but the actual type is Varies")); new RequestFieldsSnippet(Arrays.asList( fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) - .document(new OperationBuilder("mismatched-field-types", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("[{ \"a\": 5 },{ \"a\": \"b\" }]") - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build()); } @Test @@ -143,23 +133,20 @@ public void undocumentedXmlRequestField() throws IOException { this.thrown.expectMessage(startsWith( "The following parts of the payload were not" + " documented:")); new RequestFieldsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-xml-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("5").header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlRequestFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("missing-xml-request", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("5").header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("5").header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -168,15 +155,10 @@ public void missingXmlRequestField() throws IOException { 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"), - fieldWithPath("a").description("one"))) - .document( - new OperationBuilder("missing-xml-request-fields", - this.snippet.getOutputDirectory()) - .request("http://localhost") - .content("") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("one"))).document(this.operationBuilder + .request("http://localhost").content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -189,13 +171,10 @@ public void undocumentedXmlRequestFieldAndMissingXmlRequestField() .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(new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("5").header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 18e89a9b0..b88f6a51f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -50,7 +50,7 @@ public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields("map-request-with-fields") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") .row("`a`", "`Object`", "three")); @@ -58,15 +58,14 @@ public void mapRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two"), fieldWithPath("a").description("three"))) - .document(operationBuilder("map-request-with-fields") - .request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") .build()); } @Test public void arrayRequestWithFields() throws IOException { - this.snippet.expectRequestFields("array-request-with-fields") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`[]a.b`", "`Number`", "one") .row("`[]a.c`", "`String`", "two") @@ -75,8 +74,7 @@ public void arrayRequestWithFields() throws IOException { new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), fieldWithPath("[]a").description("three"))) - .document(operationBuilder("array-request-with-fields") - .request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content( "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") .build()); @@ -84,64 +82,57 @@ public void arrayRequestWithFields() throws IOException { @Test public void ignoredRequestField() throws IOException { - this.snippet.expectRequestFields("ignored-request-field") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(operationBuilder("ignored-request-field") - .request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": 5, \"b\": 4}").build()); } @Test public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { - this.snippet.expectRequestFields("ignore-all-undocumented") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), true).document( - operationBuilder("ignore-all-undocumented") - .request("http://localhost") + this.operationBuilder.request("http://localhost") .content("{\"a\": 5, \"b\": 4}").build()); } @Test public void missingOptionalRequestField() throws IOException { - this.snippet.expectRequestFields("missing-optional-request-field") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) - .document(operationBuilder("missing-optional-request-field") - .request("http://localhost").content("{}").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{}").build()); } @Test public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() throws IOException { - this.snippet - .expectRequestFields( - "missing-ignored-optional-request-field-does-not-require-a-type") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description")); new RequestFieldsSnippet(Arrays .asList(fieldWithPath("a.b").description("one").ignored().optional())) - .document(operationBuilder( - "missing-ignored-optional-request-field-does-not-require-a-type") - .request("http://localhost").content("{}") - .build()); + .document(this.operationBuilder.request("http://localhost") + .content("{}").build()); } @Test public void presentOptionalRequestField() throws IOException { - this.snippet.expectRequestFields("present-optional-request-field") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) - .document(operationBuilder("present-optional-request-field") - .request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": { \"b\": \"bravo\"}}").build()); } @@ -150,16 +141,18 @@ public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-title")); - this.snippet.expectRequestFields("request-fields-with-custom-attributes") - .withContents(containsString("Custom title")); + this.snippet.expectRequestFields().withContents(containsString("Custom title")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))).document( - operationBuilder("request-fields-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").content("{\"a\": \"foo\"}") - .build()); + attributes( + key("title").value("Custom title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content("{\"a\": \"foo\"}").build()); } @Test @@ -167,8 +160,7 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-extra-column")); - this.snippet - .expectRequestFields("request-fields-with-custom-descriptor-attributes") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description", "Foo") .row("a.b", "Number", "one", "alpha") .row("a.c", "String", "two", "bravo") @@ -181,8 +173,8 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("bravo")), fieldWithPath("a").description("three") .attributes(key("foo").value("charlie")))) - .document(operationBuilder( - "request-fields-with-custom-descriptor-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -194,35 +186,31 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { @Test public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet - .expectRequestFields("request-field-with-explicit-exactly-matching-type") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); new RequestFieldsSnippet(Arrays .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) - .document(operationBuilder( - "request-field-with-explicit-exactly-matching-type") - .request("http://localhost") - .content("{\"a\": 5 }").build()); + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": 5 }").build()); } @Test public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectRequestFields("request-field-with-explicit-varies-type") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); - new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type(JsonFieldType.VARIES))).document( - operationBuilder("request-field-with-explicit-varies-type") - .request("http://localhost").content("{\"a\": 5 }") - .build()); + new RequestFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": 5 }").build()); } @Test public void xmlRequestFields() throws IOException { - this.snippet.expectRequestFields("xml-request") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", "`a`", "three")); @@ -232,8 +220,7 @@ public void xmlRequestFields() throws IOException { fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) .document( - operationBuilder("xml-request") - .request("http://localhost") + this.operationBuilder.request("http://localhost") .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -242,7 +229,7 @@ public void xmlRequestFields() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestFields("additional-descriptors") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") .row("`a`", "`Object`", "three")); @@ -251,14 +238,13 @@ public void additionalDescriptors() throws IOException { .requestFields(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two")) .and(fieldWithPath("a").description("three")) - .document(operationBuilder("additional-descriptors") - .request("http://localhost") + .document(operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @Test public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectRequestFields("prefixed-additional-descriptors") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") .row("`a.c`", "`String`", "three")); @@ -266,23 +252,21 @@ public void prefixedAdditionalDescriptors() throws IOException { PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) - .document(operationBuilder("prefixed-additional-descriptors") - .request("http://localhost") + .document(operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @Test public void requestWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectRequestFields("request-fields-with-escaped-content") + this.snippet.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); new RequestFieldsSnippet(Arrays.asList( fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(operationBuilder("request-fields-with-escaped-content") - .request("http://localhost").content("{\"Foo|Bar\": 5}") - .build()); + .document(operationBuilder.request("http://localhost") + .content("{\"Foo|Bar\": 5}").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index bfe50ad0c..05b1b9c17 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -27,7 +27,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; @@ -35,6 +34,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.startsWith; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link ResponseFieldsSnippet} due to missing or @@ -45,7 +45,10 @@ public class ResponseFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -56,8 +59,7 @@ public void attemptToDocumentFieldsWithNoResponseBody() throws IOException { this.thrown.expectMessage( equalTo("Cannot document response fields as the response body is empty")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("no-response-body", - this.snippet.getOutputDirectory()).build()); + .document(this.operationBuilder.build()); } @Test @@ -67,9 +69,8 @@ public void fieldWithExplicitTypeThatDoesNotMatchThePayload() throws IOException + " Object but the actual type is Number")); new ResponseFieldsSnippet(Arrays .asList(fieldWithPath("a").description("one").type(JsonFieldType.OBJECT))) - .document(new OperationBuilder("mismatched-field-types", - this.snippet.getOutputDirectory()).response() - .content("{ \"a\": 5 }}").build()); + .document(this.operationBuilder.response() + .content("{ \"a\": 5 }}").build()); } @Test @@ -79,10 +80,8 @@ public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException + " Object but the actual type is Varies")); new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("[].a").description("one").type(JsonFieldType.OBJECT))) - .document(new OperationBuilder("mismatched-field-types", - this.snippet.getOutputDirectory()).response() - .content("[{ \"a\": 5 },{ \"a\": \"b\" }]") - .build()); + .document(this.operationBuilder.response() + .content("[{ \"a\": 5 },{ \"a\": \"b\" }]").build()); } @Test @@ -90,15 +89,10 @@ public void undocumentedXmlResponseField() throws IOException { this.thrown.expect(SnippetException.class); this.thrown.expectMessage(startsWith( "The following parts of the payload were not" + " documented:")); - new ResponseFieldsSnippet( - Collections.emptyList()) - .document( - new OperationBuilder("undocumented-xml-response-field", - this.snippet.getOutputDirectory()).response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet(Collections.emptyList()) + .document(this.operationBuilder.response().content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -109,8 +103,8 @@ public void missingXmlAttribute() throws IOException { new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c"))) - .document(new OperationBuilder("missing-xml-attribute", - this.snippet.getOutputDirectory()).response() + .document( + this.operationBuilder.response() .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -125,24 +119,20 @@ public void documentedXmlAttributesAreRemoved() throws IOException { + "%nbar%n"))); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a/@id").description("one").type("a"))) - .document( - new OperationBuilder("documented-attribute-is-removed", - this.snippet.getOutputDirectory()).response() - .content("bar") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.response() + .content("bar") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); } @Test public void xmlResponseFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one"))) - .document(new OperationBuilder("xml-response-no-field-type", - this.snippet.getOutputDirectory()).response().content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + .document(this.operationBuilder.response().content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -151,14 +141,10 @@ public void missingXmlResponseField() throws IOException { this.thrown.expectMessage(equalTo("Fields with the following paths were not found" + " in the payload: [a/b]")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"), - fieldWithPath("a").description("one"))) - .document( - new OperationBuilder("missing-xml-response-field", - this.snippet.getOutputDirectory()).response() - .content("") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + fieldWithPath("a").description("one"))).document(this.operationBuilder + .response().content("") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } @Test @@ -170,16 +156,10 @@ public void undocumentedXmlResponseFieldAndMissingXmlResponseField() this.thrown .expectMessage(endsWith("Fields with the following paths were not found" + " in the payload: [a/b]")); - new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a/b").description("one"))) - .document( - new OperationBuilder( - "undocumented-xml-request-field-and-missing-xml-request-field", - this.snippet.getOutputDirectory()).response() - .content("5") - .header(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_XML_VALUE) - .build()); + new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a/b").description("one"))) + .document(this.operationBuilder.response().content("5") + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) + .build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 8d93a9872..daaaa233e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -50,7 +50,7 @@ public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void mapResponseWithFields() throws IOException { - this.snippet.expectResponseFields("map-response-with-fields") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") @@ -63,7 +63,7 @@ public void mapResponseWithFields() throws IOException { fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six"))) - .document(operationBuilder("map-response-with-fields").response() + .document(this.operationBuilder.response() .content( "{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}") @@ -72,15 +72,15 @@ public void mapResponseWithFields() throws IOException { @Test public void arrayResponseWithFields() throws IOException { - this.snippet.expectResponseFields("array-response-with-fields") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`[]a.b`", "`Number`", "one") .row("`[]a.c`", "`String`", "two") .row("`[]a`", "`Object`", "three")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]a.b").description("one"), fieldWithPath("[]a.c").description("two"), - fieldWithPath("[]a").description("three"))).document( - operationBuilder("array-response-with-fields").response() + fieldWithPath("[]a").description("three"))) + .document(this.operationBuilder.response() .content( "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") .build()); @@ -88,35 +88,35 @@ public void arrayResponseWithFields() throws IOException { @Test public void arrayResponse() throws IOException { - this.snippet.expectResponseFields("array-response") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`[]`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) - .document(operationBuilder("array-response").response() + .document(this.operationBuilder.response() .content("[\"a\", \"b\", \"c\"]").build()); } @Test public void ignoredResponseField() throws IOException { - this.snippet.expectResponseFields("ignored-response-field") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").ignored(), fieldWithPath("b").description("Field b"))) - .document(operationBuilder("ignored-response-field").response() + .document(this.operationBuilder.response() .content("{\"a\": 5, \"b\": 4}").build()); } @Test public void allUndocumentedFieldsCanBeIgnored() throws IOException { - this.snippet.expectResponseFields("ignore-all-undocumented") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("b").description("Field b")), true) - .document(operationBuilder("ignore-all-undocumented").response() + .document(this.operationBuilder.response() .content("{\"a\": 5, \"b\": 4}").build()); } @@ -125,50 +125,48 @@ public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-fields")) .willReturn(snippetResource("response-fields-with-title")); - this.snippet.expectResponseFields("response-fields-with-custom-attributes") - .withContents(containsString("Custom title")); + this.snippet.expectResponseFields().withContents(containsString("Custom title")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), - attributes(key("title").value("Custom title"))).document( - operationBuilder("response-fields-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .response().content("{\"a\": \"foo\"}").build()); + attributes( + key("title").value("Custom title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response().content("{\"a\": \"foo\"}") + .build()); } @Test public void missingOptionalResponseField() throws IOException { - this.snippet.expectResponseFields("missing-optional-response-field") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") .type(JsonFieldType.STRING).optional())) - .document(operationBuilder("missing-optional-response-field") - .response().content("{}").build()); + .document(this.operationBuilder.response().content("{}").build()); } @Test public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() throws IOException { - this.snippet - .expectResponseFields( - "missing-ignored-optional-response-field-does-not-require-a-type") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description")); new ResponseFieldsSnippet(Arrays .asList(fieldWithPath("a.b").description("one").ignored().optional())) - .document(operationBuilder( - "missing-ignored-optional-response-field-does-not-require-a-type") - .response().content("{}").build()); + .document(this.operationBuilder.response().content("{}").build()); } @Test public void presentOptionalResponseField() throws IOException { - this.snippet.expectResponseFields("present-optional-response-field") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") - .type(JsonFieldType.STRING).optional())).document( - operationBuilder("present-optional-response-field").response() + .type(JsonFieldType.STRING).optional())) + .document(this.operationBuilder.response() .content("{\"a\": { \"b\": \"bravo\"}}").build()); } @@ -177,58 +175,57 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-fields")) .willReturn(snippetResource("response-fields-with-extra-column")); - this.snippet.expectResponseFields("response-fields-with-custom-attributes") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description", "Foo") .row("a.b", "Number", "one", "alpha") .row("a.c", "String", "two", "bravo") .row("a", "Object", "three", "charlie")); new ResponseFieldsSnippet(Arrays.asList( - fieldWithPath("a.b").description("one").attributes(key("foo") - .value("alpha")), + 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( - operationBuilder("response-fields-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .response() - .content( - "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") - .build()); + .attributes(key("foo").value("charlie")))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response() + .content( + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); } @Test public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet - .expectResponseFields( - "response-field-with-explicit-exactly-matching-type") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); new ResponseFieldsSnippet(Arrays .asList(fieldWithPath("a").description("one").type(JsonFieldType.NUMBER))) - .document(operationBuilder( - "response-field-with-explicit-exactly-matching-type") - .response().content("{\"a\": 5 }").build()); + .document(this.operationBuilder.response().content("{\"a\": 5 }") + .build()); } @Test public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectResponseFields("response-field-with-explicit-varies-type") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); - new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one") - .type(JsonFieldType.VARIES))).document( - operationBuilder("response-field-with-explicit-varies-type") - .response().content("{\"a\": 5 }").build()); + new ResponseFieldsSnippet(Arrays + .asList(fieldWithPath("a").description("one").type(JsonFieldType.VARIES))) + .document(this.operationBuilder.response().content("{\"a\": 5 }") + .build()); } @Test public void xmlResponseFields() throws IOException { - this.snippet.expectResponseFields("xml-response") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", "`a`", "three")); @@ -237,7 +234,7 @@ public void xmlResponseFields() throws IOException { fieldWithPath("a/c").description("two").type("c"), fieldWithPath("a").description("three").type("a"))) .document( - operationBuilder("xml-response").response() + this.operationBuilder.response() .content("5charlie") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -246,14 +243,14 @@ public void xmlResponseFields() throws IOException { @Test public void xmlAttribute() throws IOException { - this.snippet.expectResponseFields("xml-attribute") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c"))) .document( - operationBuilder("xml-attribute").response() + this.operationBuilder.response() .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) @@ -262,15 +259,15 @@ public void xmlAttribute() throws IOException { @Test public void missingOptionalXmlAttribute() throws IOException { - this.snippet.expectResponseFields("missing-optional-xml-attribute") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("b"), fieldWithPath("a/@id").description("two").type("c").optional())) .document( - operationBuilder("missing-optional-xml-attribute") - .response().content("foo") + this.operationBuilder.response() + .content("foo") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); @@ -278,12 +275,11 @@ public void missingOptionalXmlAttribute() throws IOException { @Test public void undocumentedAttributeDoesNotCauseFailure() throws IOException { - this.snippet.expectResponseFields("undocumented-attribute").withContents( + this.snippet.expectResponseFields().withContents( tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); new ResponseFieldsSnippet( - Arrays.asList(fieldWithPath("a").description("one").type("a"))) - .document(operationBuilder("undocumented-attribute").response() - .content("bar") + Arrays.asList(fieldWithPath("a").description("one").type("a"))).document( + this.operationBuilder.response().content("bar") .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE) .build()); @@ -291,7 +287,7 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseFields("additional-descriptors") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") @@ -305,7 +301,7 @@ public void additionalDescriptors() throws IOException { .and(fieldWithPath("assets[]").description("four"), fieldWithPath("assets[].id").description("five"), fieldWithPath("assets[].name").description("six")) - .document(operationBuilder("additional-descriptors").response() + .document(this.operationBuilder.response() .content("{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":" + " [{\"id\":356,\"name\": \"sample\"}]}") .build()); @@ -313,7 +309,7 @@ public void additionalDescriptors() throws IOException { @Test public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectResponseFields("prefixed-additional-descriptors") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") .row("`a.c`", "`String`", "three")); @@ -321,21 +317,21 @@ public void prefixedAdditionalDescriptors() throws IOException { PayloadDocumentation.responseFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) - .document(operationBuilder("prefixed-additional-descriptors").response() + .document(this.operationBuilder.response() .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @Test public void responseWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectResponseFields("response-fields-with-escaped-content") + this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); new ResponseFieldsSnippet(Arrays.asList( fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(operationBuilder("response-fields-with-escaped-content") - .response().content("{\"Foo|Bar\": 5}").build()); + .document(this.operationBuilder.response() + .content("{\"Foo|Bar\": 5}").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 99ab6a0ba..26f0f6b09 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -26,12 +26,12 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link PathParametersSnippet} due to missing or @@ -42,7 +42,10 @@ public class PathParametersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -53,12 +56,10 @@ public void undocumentedPathParameter() throws IOException { this.thrown.expectMessage(equalTo("Path parameters with the following names were" + " not documented: [a]")); new PathParametersSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-path-parameter", - this.snippet.getOutputDirectory()) - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/") - .build()); + .document(this.operationBuilder + .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/") + .build()); } @Test @@ -68,12 +69,9 @@ public void missingPathParameter() throws IOException { + " not found in the request: [a]")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("missing-path-parameter", - this.snippet.getOutputDirectory()) - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/") - .build()); + .document(this.operationBuilder.attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/").build()); } @Test @@ -83,13 +81,10 @@ public void undocumentedAndMissingPathParameters() throws IOException { + " 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( - new OperationBuilder("undocumented-and-missing-path-parameters", - this.snippet.getOutputDirectory()) - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{b}") - .build()); + Arrays.asList(parameterWithName("a").description("one"))) + .document(this.operationBuilder.attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{b}").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index fbc7449b5..c74f5d401 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -49,102 +49,94 @@ public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void pathParameters() throws IOException { - this.snippet.expectPathParameters("path-parameters").withContents( + this.snippet.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document(operationBuilder("path-parameters").attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}").build()); + .document(this.operationBuilder + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") + .build()); } @Test public void ignoredPathParameter() throws IOException { - this.snippet.expectPathParameters("ignored-path-parameter").withContents( + this.snippet.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document(operationBuilder("ignored-path-parameter").attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}").build()); + .document(this.operationBuilder + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}") + .build()); } @Test public void allUndocumentedPathParametersCanBeIgnored() throws IOException { - this.snippet.expectPathParameters("ignore-all-undocumented").withContents( + this.snippet.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) - .document(operationBuilder("ignore-all-undocumented").attribute( + .document(this.operationBuilder.attribute( RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}").build()); } @Test public void missingOptionalPathParameter() throws IOException { - this.snippet - .expectPathParameters( - "missing-optional-path-parameter") - .withContents(tableWithTitleAndHeader( - this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" - : "`/{a}`", - "Parameter", "Description").row("`a`", "one").row("`b`", "two")); + this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( + this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", + "Parameter", "Description").row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two").optional())).document( - operationBuilder("missing-optional-path-parameter").attribute( + parameterWithName("b").description("two").optional())) + .document(this.operationBuilder.attribute( RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}").build()); } @Test public void presentOptionalPathParameter() throws IOException { - this.snippet - .expectPathParameters( - "present-optional-path-parameter") - .withContents(tableWithTitleAndHeader( - this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" - : "`/{a}`", - "Parameter", "Description").row("`a`", "one")); + this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( + this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", + "Parameter", "Description").row("`a`", "one")); new PathParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) - .document(operationBuilder("present-optional-path-parameter") - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}") - .build()); + .document(this.operationBuilder.attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}").build()); } @Test public void pathParametersWithQueryString() throws IOException { - this.snippet.expectPathParameters("path-parameters-with-query-string") - .withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); + this.snippet.expectPathParameters().withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))).document( - operationBuilder("path-parameters-with-query-string").attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}?foo=bar").build()); + parameterWithName("b").description("two"))) + .document(this.operationBuilder + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}?foo=bar") + .build()); } @Test public void pathParametersWithQueryStringWithParameters() throws IOException { - this.snippet - .expectPathParameters("path-parameters-with-query-string-with-parameters") - .withContents( - tableWithTitleAndHeader(getTitle(), "Parameter", "Description") - .row("`a`", "one").row("`b`", "two")); + this.snippet.expectPathParameters().withContents( + tableWithTitleAndHeader(getTitle(), "Parameter", "Description") + .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), parameterWithName("b").description("two"))) - .document(operationBuilder( - "path-parameters-with-query-string-with-parameters") - .attribute( - RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, - "/{a}/{b}?foo={c}") - .build()); + .document(this.operationBuilder + .attribute( + RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, + "/{a}/{b}?foo={c}") + .build()); } @Test @@ -152,8 +144,7 @@ public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")) .willReturn(snippetResource("path-parameters-with-title")); - this.snippet.expectPathParameters("path-parameters-with-custom-attributes") - .withContents(containsString("The title")); + this.snippet.expectPathParameters().withContents(containsString("The title")); new PathParametersSnippet( Arrays.asList( @@ -161,8 +152,8 @@ public void pathParametersWithCustomAttributes() throws IOException { .attributes(key("foo").value("alpha")), parameterWithName("b").description("two") .attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))).document( - operationBuilder("path-parameters-with-custom-attributes") + attributes(key("title").value("The title"))) + .document(this.operationBuilder .attribute( RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") @@ -177,17 +168,16 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")) .willReturn(snippetResource("path-parameters-with-extra-column")); - this.snippet - .expectPathParameters("path-parameters-with-custom-descriptor-attributes") + this.snippet.expectPathParameters() .withContents(tableWithHeader("Parameter", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); new PathParametersSnippet(Arrays.asList( parameterWithName("a").description("one") .attributes(key("foo").value("alpha")), - parameterWithName("b").description("two").attributes( - key("foo").value("bravo")))).document(operationBuilder( - "path-parameters-with-custom-descriptor-attributes") + parameterWithName("b").description("two") + .attributes(key("foo").value("bravo")))) + .document(this.operationBuilder .attribute( RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") @@ -198,12 +188,12 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectPathParameters("additional-descriptors").withContents( + this.snippet.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); RequestDocumentation.pathParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document(operationBuilder("additional-descriptors") + .document(this.operationBuilder .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "/{a}/{b}") .build()); @@ -211,14 +201,14 @@ public void additionalDescriptors() throws IOException { @Test public void pathParametersWithEscapedContent() throws IOException { - this.snippet.expectPathParameters("path-parameters-with-escaped-content") + this.snippet.expectPathParameters() .withContents(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); RequestDocumentation .pathParameters(parameterWithName("Foo|Bar").description("one|two")) - .document(operationBuilder("path-parameters-with-escaped-content") + .document(this.operationBuilder .attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, "{Foo|Bar}") .build()); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java index 79cea42b5..ce50cc892 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -25,12 +25,12 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link RequestParametersSnippet} due to missing or @@ -41,7 +41,10 @@ public class RequestParametersSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -53,9 +56,8 @@ public void undocumentedParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not documented: [a]")); new RequestParametersSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-parameter", - this.snippet.getOutputDirectory()).request("http://localhost") - .param("a", "alpha").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "alpha").build()); } @Test @@ -65,10 +67,8 @@ public void missingParameter() throws IOException { .expectMessage(equalTo("Request parameters with the following names were" + " not found in the request: [a]")); new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))) - .document(new OperationBuilder("missing-parameter", - this.snippet.getOutputDirectory()) - .request("http://localhost").build()); + Arrays.asList(parameterWithName("a").description("one"))).document( + this.operationBuilder.request("http://localhost").build()); } @Test @@ -79,11 +79,9 @@ public void undocumentedAndMissingParameters() throws IOException { + " not documented: [b]. Request parameters with the following" + " names were not found in the request: [a]")); new RequestParametersSnippet( - Arrays.asList(parameterWithName("a").description("one"))).document( - new OperationBuilder("undocumented-and-missing-parameters", - this.snippet.getOutputDirectory()) - .request("http://localhost").param("b", "bravo") - .build()); + Arrays.asList(parameterWithName("a").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .param("b", "bravo").build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index 78dac57dd..cffbb671a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -48,72 +48,66 @@ public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) @Test public void requestParameters() throws IOException { - this.snippet.expectRequestParameters("request-parameters") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"), - parameterWithName("b").description("two"))) - .document(operationBuilder("request-parameters") - .request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); + parameterWithName("b").description("two"))).document( + this.operationBuilder.request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test public void requestParameterWithNoValue() throws IOException { - this.snippet.expectRequestParameters("request-parameter-with-no-value") - .withContents( - tableWithHeader("Parameter", "Description").row("`a`", "one")); + this.snippet.expectRequestParameters().withContents( + tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) - .document(operationBuilder("request-parameter-with-no-value") - .request("http://localhost").param("a").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a").build()); } @Test public void ignoredRequestParameter() throws IOException { - this.snippet.expectRequestParameters("ignored-request-parameter").withContents( + this.snippet.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) - .document(operationBuilder("ignored-request-parameter") - .request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { - this.snippet.expectRequestParameters("ignore-all-undocumented").withContents( + this.snippet.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) - .document(operationBuilder("ignore-all-undocumented") - .request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test public void missingOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters("missing-optional-request-parameter") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional(), parameterWithName("b").description("two"))).document( - operationBuilder("missing-optional-request-parameter") - .request("http://localhost").param("b", "bravo") - .build()); + this.operationBuilder.request("http://localhost") + .param("b", "bravo").build()); } @Test public void presentOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters("present-optional-request-parameter") - .withContents( - tableWithHeader("Parameter", "Description").row("`a`", "one")); + this.snippet.expectRequestParameters().withContents( + tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) - .document(operationBuilder("present-optional-request-parameter") - .request("http://localhost").param("a", "one").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "one").build()); } @Test @@ -121,8 +115,7 @@ public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-title")); - this.snippet.expectRequestParameters("request-parameters-with-custom-attributes") - .withContents(containsString("The title")); + this.snippet.expectRequestParameters().withContents(containsString("The title")); new RequestParametersSnippet( Arrays.asList( @@ -130,12 +123,16 @@ public void requestParametersWithCustomAttributes() throws IOException { .attributes(key("foo").value("alpha")), parameterWithName("b").description("two") .attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))).document( - operationBuilder("request-parameters-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").param("a", "alpha") - .param("b", "bravo").build()); + attributes( + key("title").value("The title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .param("a", "alpha").param("b", "bravo") + .build()); } @Test @@ -143,9 +140,7 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-extra-column")); - this.snippet - .expectRequestParameters( - "request-parameters-with-custom-descriptor-attributes") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -154,8 +149,8 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException .attributes(key("foo").value("alpha")), parameterWithName("b").description("two") .attributes(key("foo").value("bravo")))) - .document(operationBuilder( - "request-parameters-with-custom-descriptor-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -169,42 +164,44 @@ public void requestParametersWithOptionalColumn() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-optional-column")); - this.snippet.expectRequestParameters("request-parameters-with-optional-column") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Optional", "Description") .row("a", "true", "one").row("b", "false", "two")); - new RequestParametersSnippet(Arrays.asList( - parameterWithName("a").description("one").optional(), - parameterWithName("b").description("two"))).document( - operationBuilder("request-parameters-with-optional-column") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").param("a", "alpha") - .param("b", "bravo").build()); + new RequestParametersSnippet( + Arrays.asList(parameterWithName("a").description("one").optional(), + parameterWithName("b").description("two"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .param("a", "alpha").param("b", "bravo") + .build()); } @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParameters("additional-descriptors") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); RequestDocumentation.requestParameters(parameterWithName("a").description("one")) .and(parameterWithName("b").description("two")) - .document(operationBuilder("additional-descriptors") - .request("http://localhost").param("a", "bravo") - .param("b", "bravo").build()); + .document(this.operationBuilder.request("http://localhost") + .param("a", "bravo").param("b", "bravo").build()); } @Test public void requestParametersWithEscapedContent() throws IOException { - this.snippet.expectRequestParameters("request-parameters-with-escaped-content") + this.snippet.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); RequestDocumentation .requestParameters(parameterWithName("Foo|Bar").description("one|two")) - .document(operationBuilder("request-parameters-with-escaped-content") - .request("http://localhost").param("Foo|Bar", "baz").build()); + .document(this.operationBuilder.request("http://localhost") + .param("Foo|Bar", "baz").build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java index b349f0f03..522598201 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java @@ -25,12 +25,12 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor; /** * Tests for failures when rendering {@link RequestPartsSnippet} due to missing or @@ -41,7 +41,10 @@ public class RequestPartsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -52,9 +55,8 @@ public void undocumentedPart() throws IOException { this.thrown.expectMessage(equalTo( "Request parts with the following names were" + " not documented: [a]")); new RequestPartsSnippet(Collections.emptyList()) - .document(new OperationBuilder("undocumented-part", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("a", "alpha".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "alpha".getBytes()).build()); } @Test @@ -63,9 +65,7 @@ public void missingPart() throws IOException { this.thrown.expectMessage(equalTo("Request parts with the following names were" + " not found in the request: [a]")); new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) - .document(new OperationBuilder("missing-part", - this.snippet.getOutputDirectory()).request("http://localhost") - .build()); + .document(this.operationBuilder.request("http://localhost").build()); } @Test @@ -75,9 +75,8 @@ public void undocumentedAndMissingParts() throws IOException { + " not documented: [b]. Request parts with the following" + " names were not found in the request: [a]")); new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"))) - .document(new OperationBuilder("undocumented-and-missing-parts", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("b", "bravo".getBytes()).build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java index 6468d80b6..ecdce573a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -48,59 +48,57 @@ public RequestPartsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestParts() throws IOException { - this.snippet.expectRequestParts("request-parts") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"), partWithName("b").description("two"))) - .document(operationBuilder("request-parts") - .request("http://localhost").part("a", "bravo".getBytes()) - .and().part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()).and() + .part("b", "bravo".getBytes()).build()); } @Test public void ignoredRequestPart() throws IOException { - this.snippet.expectRequestParts("ignored-request-part") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(), partWithName("b").description("two"))) - .document(operationBuilder("ignored-request-part") - .request("http://localhost").part("a", "bravo".getBytes()) - .and().part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()).and() + .part("b", "bravo".getBytes()).build()); } @Test public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { - this.snippet.expectRequestParts("ignore-all-undocumented") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("b").description("two")), true) - .document(operationBuilder("ignore-all-undocumented") - .request("http://localhost").part("a", "bravo".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()).and().part("b", "bravo".getBytes()) + .build()); } @Test public void missingOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts("missing-optional-request-parts") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional(), partWithName("b").description("two"))).document( - operationBuilder("missing-optional-request-parts") - .request("http://localhost") + this.operationBuilder.request("http://localhost") .part("b", "bravo".getBytes()).build()); } @Test public void presentOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts("present-optional-request-part") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one")); new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional())) - .document(operationBuilder("present-optional-request-part") - .request("http://localhost").part("a", "one".getBytes()) - .build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "one".getBytes()).build()); } @Test @@ -108,8 +106,7 @@ public void requestPartsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-title")); - this.snippet.expectRequestParts("request-parts-with-custom-attributes") - .withContents(containsString("The title")); + this.snippet.expectRequestParts().withContents(containsString("The title")); new RequestPartsSnippet( Arrays.asList( @@ -117,12 +114,16 @@ public void requestPartsWithCustomAttributes() throws IOException { .attributes(key("foo").value("alpha")), partWithName("b").description("two") .attributes(key("foo").value("bravo"))), - attributes(key("title").value("The title"))) - .document(operationBuilder("request-parts-with-custom-attributes") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost").part("a", "alpha".getBytes()) - .and().part("b", "bravo".getBytes()).build()); + attributes( + key("title").value("The title"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .part("a", "alpha".getBytes()).and() + .part("b", "bravo".getBytes()).build()); } @Test @@ -130,7 +131,7 @@ public void requestPartsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-extra-column")); - this.snippet.expectRequestParts("request-parts-with-custom-descriptor-attributes") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -139,8 +140,8 @@ public void requestPartsWithCustomDescriptorAttributes() throws IOException { .attributes(key("foo").value("alpha")), partWithName("b").description("two") .attributes(key("foo").value("bravo")))) - .document(operationBuilder( - "request-parts-with-custom-descriptor-attributes") + .document( + this.operationBuilder .attribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( resolver)) @@ -154,43 +155,44 @@ public void requestPartsWithOptionalColumn() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-optional-column")); - this.snippet.expectRequestParts("request-parts-with-optional-column") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Optional", "Description") .row("a", "true", "one").row("b", "false", "two")); new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional(), - partWithName("b").description("two"))).document( - operationBuilder("request-parts-with-optional-column") - .attribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(resolver)) - .request("http://localhost") - .part("a", "alpha".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + partWithName("b").description("two"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .part("a", "alpha".getBytes()).and() + .part("b", "bravo".getBytes()).build()); } @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParts("additional-descriptors") + this.snippet.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); RequestDocumentation.requestParts(partWithName("a").description("one")) .and(partWithName("b").description("two")) - .document(operationBuilder("additional-descriptors") - .request("http://localhost").part("a", "bravo".getBytes()).and() - .part("b", "bravo".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("a", "bravo".getBytes()).and().part("b", "bravo".getBytes()) + .build()); } @Test public void requestPartsWithEscapedContent() throws IOException { - this.snippet.expectRequestParts("request-parts-with-escaped-content") - .withContents(tableWithHeader("Part", "Description").row( - escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); + this.snippet.expectRequestParts().withContents( + tableWithHeader("Part", "Description").row(escapeIfNecessary("`Foo|Bar`"), + escapeIfNecessary("one|two"))); RequestDocumentation.requestParts(partWithName("Foo|Bar").description("one|two")) - .document(operationBuilder("request-parts-with-escaped-content") - .request("http://localhost").part("Foo|Bar", "baz".getBytes()) - .build()); + .document(this.operationBuilder.request("http://localhost") + .part("Foo|Bar", "baz".getBytes()).build()); } private String escapeIfNecessary(String input) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index bc1c77856..e5975f86d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -71,68 +71,67 @@ private void verifySnippet() throws IOException { } } - public ExpectedSnippet expectCurlRequest(String name) { - expect(name, "curl-request"); + public ExpectedSnippet expectCurlRequest() { + expect("curl-request"); return this; } - public ExpectedSnippet expectHttpieRequest(String name) { - expect(name, "httpie-request"); + public ExpectedSnippet expectHttpieRequest() { + expect("httpie-request"); return this; } - public ExpectedSnippet expectRequestFields(String name) { - expect(name, "request-fields"); + public ExpectedSnippet expectRequestFields() { + expect("request-fields"); return this; } - public ExpectedSnippet expectResponseFields(String name) { - expect(name, "response-fields"); + public ExpectedSnippet expectResponseFields() { + expect("response-fields"); return this; } - public ExpectedSnippet expectRequestHeaders(String name) { - expect(name, "request-headers"); + public ExpectedSnippet expectRequestHeaders() { + expect("request-headers"); return this; } - public ExpectedSnippet expectResponseHeaders(String name) { - expect(name, "response-headers"); + public ExpectedSnippet expectResponseHeaders() { + expect("response-headers"); return this; } - public ExpectedSnippet expectLinks(String name) { - expect(name, "links"); + public ExpectedSnippet expectLinks() { + expect("links"); return this; } - public ExpectedSnippet expectHttpRequest(String name) { - expect(name, "http-request"); + public ExpectedSnippet expectHttpRequest() { + expect("http-request"); return this; } - public ExpectedSnippet expectHttpResponse(String name) { - expect(name, "http-response"); + public ExpectedSnippet expectHttpResponse() { + expect("http-response"); return this; } - public ExpectedSnippet expectRequestParameters(String name) { - expect(name, "request-parameters"); + public ExpectedSnippet expectRequestParameters() { + expect("request-parameters"); return this; } - public ExpectedSnippet expectPathParameters(String name) { - expect(name, "path-parameters"); + public ExpectedSnippet expectPathParameters() { + expect("path-parameters"); return this; } - public ExpectedSnippet expectRequestParts(String name) { - expect(name, "request-parts"); + public ExpectedSnippet expectRequestParts() { + expect("request-parts"); return this; } - private ExpectedSnippet expect(String name, String type) { - this.expectedName = name; + private ExpectedSnippet expect(String type) { this.expectedType = type; return this; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 3032a6f5d..8acc87b0f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -24,6 +24,10 @@ import java.util.List; import java.util.Map; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -45,7 +49,6 @@ import org.springframework.restdocs.templates.StandardTemplateResourceResolver; import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.templates.TemplateFormats; import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda; import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; @@ -54,28 +57,21 @@ * * @author Andy Wilkinson */ -public class OperationBuilder { +public class OperationBuilder implements TestRule { private final Map attributes = new HashMap<>(); - private final OperationResponseBuilder responseBuilder = new OperationResponseBuilder(); + private OperationResponseBuilder responseBuilder; - private final String name; + private String name; - private final File outputDirectory; + private File outputDirectory; private final TemplateFormat templateFormat; private OperationRequestBuilder requestBuilder; - public OperationBuilder(String name, File outputDirectory) { - this(name, outputDirectory, TemplateFormats.asciidoctor()); - } - - public OperationBuilder(String name, File outputDirectory, - TemplateFormat templateFormat) { - this.name = name; - this.outputDirectory = outputDirectory; + public OperationBuilder(TemplateFormat templateFormat) { this.templateFormat = templateFormat; } @@ -85,6 +81,7 @@ public OperationRequestBuilder request(String uri) { } public OperationResponseBuilder response() { + this.responseBuilder = new OperationResponseBuilder(); return this.responseBuilder; } @@ -93,6 +90,14 @@ public OperationBuilder attribute(String name, Object value) { return this; } + private void prepare(String operationName, File outputDirectory) { + this.name = operationName; + this.outputDirectory = outputDirectory; + this.requestBuilder = null; + this.requestBuilder = null; + this.attributes.clear(); + } + public Operation build() { if (this.attributes.get(TemplateEngine.class.getName()) == null) { Map templateContext = new HashMap<>(); @@ -113,7 +118,10 @@ public Operation build() { (this.requestBuilder == null ? new OperationRequestBuilder("http://localhost/").buildRequest() : this.requestBuilder.buildRequest()), - this.responseBuilder.buildResponse(), this.attributes); + this.responseBuilder == null + ? new OperationResponseBuilder().buildResponse() + : this.responseBuilder.buildResponse(), + this.attributes); } private RestDocumentationContext createContext() { @@ -124,6 +132,25 @@ private RestDocumentationContext createContext() { return context; } + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + + @Override + public void evaluate() throws Throwable { + String operationName = description.getMethodName(); + int index = operationName.indexOf('['); + if (index > 0) { + operationName = operationName.substring(0, index); + } + OperationBuilder.this.prepare(operationName, + new File("build/" + description.getTestClass().getSimpleName())); + base.evaluate(); + } + + }; + } + /** * Basic builder API for creating an {@link OperationRequest}. */ From 4e4d01a459d3c2972fb1f4a7220978b19ffec165 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Aug 2016 12:49:03 +0100 Subject: [PATCH 178/898] Consolidate logic in test rules for getting output dir and operation name --- .../headers/RequestHeadersSnippetTests.java | 2 + .../headers/ResponseHeadersSnippetTests.java | 2 + .../restdocs/test/ExpectedSnippet.java | 11 ++-- .../restdocs/test/OperationBuilder.java | 23 ++------ .../restdocs/test/OperationTestRule.java | 54 +++++++++++++++++++ 5 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 805c1f7cb..373e83b70 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -93,6 +93,8 @@ public void caseInsensitiveRequestHeaders() throws IOException { @Test public void undocumentedRequestHeader() throws IOException { + this.snippet.expectRequestHeaders().withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) .document(this.operationBuilder.request("http://localhost") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index 86ac3eef6..ba572598d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -81,6 +81,8 @@ public void caseInsensitiveResponseHeaders() throws IOException { @Test public void undocumentedResponseHeader() throws IOException { + this.snippet.expectResponseHeaders().withContents( + tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))).document( this.operationBuilder.response().header("X-Test", "test") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java index e5975f86d..8036e98e0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java @@ -20,8 +20,6 @@ import java.io.IOException; 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.TemplatedSnippet; @@ -38,7 +36,7 @@ * @author Andy Wilkinson * @author Andreas Evers */ -public class ExpectedSnippet implements TestRule { +public class ExpectedSnippet extends OperationTestRule { private final TemplateFormat templateFormat; @@ -56,9 +54,10 @@ public ExpectedSnippet(TemplateFormat templateFormat) { } @Override - public Statement apply(final Statement base, Description description) { - this.outputDirectory = new File( - "build/" + description.getTestClass().getSimpleName()); + public Statement apply(final Statement base, File outputDirectory, + String operationName) { + this.outputDirectory = outputDirectory; + this.expectedName = operationName; return new ExpectedSnippetStatement(base); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 8acc87b0f..698a80dac 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -24,8 +24,6 @@ import java.util.List; import java.util.Map; -import org.junit.rules.TestRule; -import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.springframework.http.HttpHeaders; @@ -57,7 +55,7 @@ * * @author Andy Wilkinson */ -public class OperationBuilder implements TestRule { +public class OperationBuilder extends OperationTestRule { private final Map attributes = new HashMap<>(); @@ -133,22 +131,9 @@ private RestDocumentationContext createContext() { } @Override - public Statement apply(final Statement base, final Description description) { - return new Statement() { - - @Override - public void evaluate() throws Throwable { - String operationName = description.getMethodName(); - int index = operationName.indexOf('['); - if (index > 0) { - operationName = operationName.substring(0, index); - } - OperationBuilder.this.prepare(operationName, - new File("build/" + description.getTestClass().getSimpleName())); - base.evaluate(); - } - - }; + public Statement apply(Statement base, File outputDirectory, String operationName) { + prepare(operationName, outputDirectory); + return base; } /** diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java new file mode 100644 index 000000000..28ea176d5 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationTestRule.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2016 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.test; + +import java.io.File; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * Abstract base class for Operation-related {@link TestRule TestRules}. + * + * @author Andy Wilkinson + */ +abstract class OperationTestRule implements TestRule { + + @Override + public final Statement apply(Statement base, Description description) { + return apply(base, determineOutputDirectory(description), + determineOperationName(description)); + } + + private File determineOutputDirectory(Description description) { + return new File("build/" + description.getTestClass().getSimpleName()); + } + + private String determineOperationName(Description description) { + String operationName = description.getMethodName(); + int index = operationName.indexOf('['); + if (index > 0) { + operationName = operationName.substring(0, index); + } + return operationName; + } + + protected abstract Statement apply(Statement base, File outputDirectory, + String operationName); + +} From 0f1f84e6a604532a37e1f760c24629009f8ea424 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Aug 2016 12:29:14 +0100 Subject: [PATCH 179/898] Fix JSON field type resolution for top-level and nested array fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the logic that determined the field’s type would incorrectly look at the contents of the array. For example, if the array contained one or more objects, the field’s type would be resolved as Object rather than Array. This commit updates JsonFieldPath to record when a path explicitly identifies an array so that the array itself is used to determine the field’s type rather than its contents. Closes gh-292 --- .../restdocs/payload/JsonFieldPath.java | 19 ++++++-- .../restdocs/payload/JsonFieldProcessor.java | 2 +- .../payload/JsonFieldTypeResolver.java | 2 +- .../restdocs/payload/JsonFieldPathTests.java | 48 ++++++++++++------- .../payload/JsonFieldProcessorTests.java | 12 +++++ .../payload/JsonFieldTypeResolverTests.java | 17 +++++++ .../payload/RequestFieldsSnippetTests.java | 21 ++++---- .../payload/ResponseFieldsSnippetTests.java | 6 +-- 8 files changed, 92 insertions(+), 35 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java index 10478a839..03ddfefb3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldPath.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.payload; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -42,16 +43,24 @@ final class JsonFieldPath { private final boolean precise; - private JsonFieldPath(String rawPath, List segments, boolean precise) { + private final boolean array; + + private JsonFieldPath(String rawPath, List segments, boolean precise, + boolean array) { this.rawPath = rawPath; this.segments = segments; this.precise = precise; + this.array = array; } boolean isPrecise() { return this.precise; } + boolean isArray() { + return this.array; + } + List getSegments() { return this.segments; } @@ -63,7 +72,8 @@ public String toString() { static JsonFieldPath compile(String path) { List segments = extractSegments(path); - return new JsonFieldPath(path, segments, matchesSingleValue(segments)); + return new JsonFieldPath(path, segments, matchesSingleValue(segments), + isArraySegment(segments.get(segments.size() - 1))); } static boolean isArraySegment(String segment) { @@ -71,8 +81,9 @@ static boolean isArraySegment(String segment) { } static boolean matchesSingleValue(List segments) { - for (String segment : segments) { - if (isArraySegment(segment)) { + Iterator iterator = segments.iterator(); + while (iterator.hasNext()) { + if (isArraySegment(iterator.next()) && iterator.hasNext()) { return false; } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index fb63a2148..dbaf6d88c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -57,7 +57,7 @@ public void foundMatch(Match match) { if (matches.isEmpty()) { throw new FieldDoesNotExistException(path); } - if (path.isPrecise()) { + if ((!path.isArray()) && path.isPrecise()) { return matches.get(0); } else { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java index f5bc6ec3e..25c373cd5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldTypeResolver.java @@ -44,7 +44,7 @@ else if (fieldType != commonType) { } return commonType; } - return determineFieldType(this.fieldProcessor.extract(fieldPath, payload)); + return determineFieldType(field); } private JsonFieldType determineFieldType(Object fieldValue) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java index 043977968..dc71f15ef 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldPathTests.java @@ -32,43 +32,59 @@ public class JsonFieldPathTests { @Test - public void singleFieldIsPrecise() { - assertTrue(JsonFieldPath.compile("a").isPrecise()); + public void singleFieldIsPreciseAndNotAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a"); + assertTrue(path.isPrecise()); + assertFalse(path.isArray()); } @Test - public void singleNestedFieldIsPrecise() { - assertTrue(JsonFieldPath.compile("a.b").isPrecise()); + public void singleNestedFieldIsPreciseAndNotAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a.b"); + assertTrue(path.isPrecise()); + assertFalse(path.isArray()); } @Test - public void topLevelArrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("[]").isPrecise()); + public void topLevelArrayIsPreciseAndAnArray() { + JsonFieldPath path = JsonFieldPath.compile("[]"); + assertTrue(path.isPrecise()); + assertTrue(path.isArray()); } @Test - public void fieldBeneathTopLevelArrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("[]a").isPrecise()); + public void fieldBeneathTopLevelArrayIsNotPreciseAndNotAnArray() { + JsonFieldPath path = JsonFieldPath.compile("[]a"); + assertFalse(path.isPrecise()); + assertFalse(path.isArray()); } @Test - public void arrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("a[]").isPrecise()); + public void arrayIsPreciseAndAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a[]"); + assertTrue(path.isPrecise()); + assertTrue(path.isArray()); } @Test - public void nestedArrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("a.b[]").isPrecise()); + public void nestedArrayIsPreciseAndAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a.b[]"); + assertTrue(path.isPrecise()); + assertTrue(path.isArray()); } @Test - public void arrayOfArraysIsNotPrecise() { - assertFalse(JsonFieldPath.compile("a[][]").isPrecise()); + public void arrayOfArraysIsNotPreciseAndIsAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a[][]"); + assertFalse(path.isPrecise()); + assertTrue(path.isArray()); } @Test - public void fieldBeneathAnArrayIsNotPrecise() { - assertFalse(JsonFieldPath.compile("a[].b").isPrecise()); + public void fieldBeneathAnArrayIsNotPreciseAndIsNotAnArray() { + JsonFieldPath path = JsonFieldPath.compile("a[].b"); + assertFalse(path.isPrecise()); + assertFalse(path.isArray()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index 01af20974..395ca4dfb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -55,6 +56,17 @@ public void extractNestedMapEntry() { equalTo((Object) "bravo")); } + @Test + public void extractTopLevelArray() { + List> payload = new ArrayList<>(); + Map bravo = new HashMap<>(); + bravo.put("b", "bravo"); + payload.add(bravo); + payload.add(bravo); + assertThat(this.fieldProcessor.extract(JsonFieldPath.compile("[]"), payload), + equalTo((Object) payload)); + } + @Test public void extractArray() { Map payload = new HashMap<>(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java index 502bfd29c..e1ba3ec52 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldTypeResolverTests.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; @@ -45,6 +46,22 @@ public void arrayField() throws IOException { assertFieldType(JsonFieldType.ARRAY, "[]"); } + @Test + public void topLevelArray() throws IOException { + assertThat( + this.fieldTypeResolver.resolveFieldType("[]", + new ObjectMapper().readValue("[{\"a\":\"alpha\"}]", List.class)), + equalTo(JsonFieldType.ARRAY)); + } + + @Test + public void nestedArray() throws IOException { + assertThat( + this.fieldTypeResolver.resolveFieldType("a[]", + createPayload("{\"a\": [{\"b\":\"bravo\"}]}")), + equalTo(JsonFieldType.ARRAY)); + } + @Test public void booleanField() throws IOException { assertFieldType(JsonFieldType.BOOLEAN, "true"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index b88f6a51f..8970a43da 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -67,13 +67,14 @@ public void mapRequestWithFields() throws IOException { public void arrayRequestWithFields() throws IOException { this.snippet.expectRequestFields() .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"))) + .row("`[]`", "`Array`", "one").row("`[]a.b`", "`Number`", "two") + .row("`[]a.c`", "`String`", "three") + .row("`[]a`", "`Object`", "four")); + + new RequestFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"), + fieldWithPath("[]a.b").description("two"), + fieldWithPath("[]a.c").description("three"), + fieldWithPath("[]a").description("four"))) .document(this.operationBuilder.request("http://localhost") .content( "[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]") @@ -238,7 +239,7 @@ public void additionalDescriptors() throws IOException { .requestFields(fieldWithPath("a.b").description("one"), fieldWithPath("a.c").description("two")) .and(fieldWithPath("a").description("three")) - .document(operationBuilder.request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @@ -252,7 +253,7 @@ public void prefixedAdditionalDescriptors() throws IOException { PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) .andWithPrefix("a.", fieldWithPath("b").description("two"), fieldWithPath("c").description("three")) - .document(operationBuilder.request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } @@ -265,7 +266,7 @@ public void requestWithFieldsWithEscapedContent() throws IOException { new RequestFieldsSnippet(Arrays.asList( fieldWithPath("Foo|Bar").type("one|two").description("three|four"))) - .document(operationBuilder.request("http://localhost") + .document(this.operationBuilder.request("http://localhost") .content("{\"Foo|Bar\": 5}").build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index daaaa233e..d22fdb332 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -54,7 +54,7 @@ public void mapResponseWithFields() throws IOException { .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") - .row("`assets[]`", "`Object`", "four") + .row("`assets[]`", "`Array`", "four") .row("`assets[].id`", "`Number`", "five") .row("`assets[].name`", "`String`", "six")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("id").description("one"), @@ -90,7 +90,7 @@ public void arrayResponseWithFields() throws IOException { public void arrayResponse() throws IOException { this.snippet.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`[]`", - "`String`", "one")); + "`Array`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) .document(this.operationBuilder.response() .content("[\"a\", \"b\", \"c\"]").build()); @@ -291,7 +291,7 @@ public void additionalDescriptors() throws IOException { .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") - .row("`assets[]`", "`Object`", "four") + .row("`assets[]`", "`Array`", "four") .row("`assets[].id`", "`Number`", "five") .row("`assets[].name`", "`String`", "six")); PayloadDocumentation From c0e950b3ee7682b7936e213ccf4e68ea13cbb551 Mon Sep 17 00:00:00 2001 From: Spring Buildmaster Date: Wed, 31 Aug 2016 08:47:58 +0000 Subject: [PATCH 180/898] Next development version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index af18773ba..cfc375de4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.2.BUILD-SNAPSHOT +version=1.1.3.BUILD-SNAPSHOT org.gradle.jvmargs=-XX:MaxPermSize=512M From 3ac4a1acada45cacc0a7d394edf719315f26b054 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Sun, 9 Oct 2016 12:54:15 +0200 Subject: [PATCH 181/898] Provide a default output directory for snippets based on build tool Rather than requiring an output directory to be explcitly configured, a default is now automatically configured based on the build tool that's being used. When using Gradle, snippets will be generated in build/generated-snippets. When using Maven, snippets will be generated in target/generated-snippets. See gh-297 --- config/checkstyle/checkstyle-suppressions.xml | 1 + .../mockmvc/CustomDefaultSnippets.java | 3 +- .../com/example/mockmvc/CustomEncoding.java | 2 +- .../com/example/mockmvc/CustomFormat.java | 2 +- .../mockmvc/CustomUriConfiguration.java | 2 +- .../mockmvc/EveryTestPreprocessing.java | 3 +- .../ExampleApplicationTestNgTests.java | 3 +- .../mockmvc/ExampleApplicationTests.java | 3 +- .../example/mockmvc/ParameterizedOutput.java | 2 +- .../restassured/CustomDefaultSnippets.java | 3 +- .../example/restassured/CustomEncoding.java | 2 +- .../com/example/restassured/CustomFormat.java | 2 +- .../restassured/EveryTestPreprocessing.java | 3 +- .../ExampleApplicationTestNgTests.java | 3 +- .../restassured/ExampleApplicationTests.java | 3 +- .../restassured/ParameterizedOutput.java | 3 +- .../SampleRestAssuredApplicationTests.java | 2 +- .../com/example/notes/ApiDocumentation.java | 2 +- .../com/example/notes/ApiDocumentation.java | 2 +- .../notes/GettingStartedDocumentation.java | 2 +- .../com/example/notes/ApiDocumentation.java | 2 +- .../notes/GettingStartedDocumentation.java | 2 +- .../testng/SampleTestNgApplicationTests.java | 2 +- settings.gradle | 3 +- .../build.gradle | 6 ++ .../extensions/RestDocsExtensionRegistry.java | 45 +++++++++++++ .../extensions/RestDocsSnippetBlockMacro.java | 66 +++++++++++++++++++ ...sciidoctor.extension.spi.ExtensionRegistry | 1 + .../RestDocsSnippetBlockMacroTest.java | 54 +++++++++++++++ .../src/test/resources/rest_docs_macro.adoc | 1 + .../restdocs/JUnitRestDocumentation.java | 8 +++ .../restdocs/ManualRestDocumentation.java | 23 +++++++ .../restdocs/RestDocumentation.java | 8 +++ .../RestDocumentationConfigurerTests.java | 3 +- ...tationContextPlaceholderResolverTests.java | 3 +- .../snippet/StandardWriterResolverTests.java | 3 +- .../restdocs/test/OperationBuilder.java | 3 +- ...ckMvcRestDocumentationConfigurerTests.java | 2 +- ...kMvcRestDocumentationIntegrationTests.java | 3 +- ...suredRestDocumentationConfigurerTests.java | 3 +- ...uredRestDocumentationIntegrationTests.java | 3 +- 41 files changed, 245 insertions(+), 47 deletions(-) create mode 100644 spring-restdocs-asciidoctor-extensions/build.gradle create mode 100644 spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java create mode 100644 spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java create mode 100644 spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry create mode 100644 spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java create mode 100644 spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index f74d92343..2c9a1795b 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -5,4 +5,5 @@ + diff --git a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java index 6c974927c..1644912e9 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/mockmvc/CustomDefaultSnippets.java @@ -31,8 +31,7 @@ public class CustomDefaultSnippets { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java index 0351216aa..86bbe0daf 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomEncoding.java +++ b/docs/src/test/java/com/example/mockmvc/CustomEncoding.java @@ -29,7 +29,7 @@ public class CustomEncoding { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomFormat.java b/docs/src/test/java/com/example/mockmvc/CustomFormat.java index b0cae3b18..1e85a49aa 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomFormat.java +++ b/docs/src/test/java/com/example/mockmvc/CustomFormat.java @@ -30,7 +30,7 @@ public class CustomFormat { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java index bc3e0e75f..d71549a89 100644 --- a/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java +++ b/docs/src/test/java/com/example/mockmvc/CustomUriConfiguration.java @@ -29,7 +29,7 @@ public class CustomUriConfiguration { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java index 31d883549..934a353e7 100644 --- a/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/mockmvc/EveryTestPreprocessing.java @@ -38,8 +38,7 @@ public class EveryTestPreprocessing { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); private WebApplicationContext context; diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java index fac8c5ca8..5bec235e4 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTestNgTests.java @@ -30,8 +30,7 @@ public class ExampleApplicationTestNgTests { - public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( - "target/generated-snippets"); + public final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); @SuppressWarnings("unused") // tag::setup[] private MockMvc mockMvc; diff --git a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java index 21afd53d3..61a361182 100644 --- a/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/mockmvc/ExampleApplicationTests.java @@ -30,8 +30,7 @@ public class ExampleApplicationTests { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") // tag::setup[] diff --git a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java index d39102bff..3011f4163 100644 --- a/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/mockmvc/ParameterizedOutput.java @@ -29,7 +29,7 @@ public class ParameterizedOutput { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private MockMvc mockMvc; diff --git a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java index 27512e649..39544db0f 100644 --- a/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java +++ b/docs/src/test/java/com/example/restassured/CustomDefaultSnippets.java @@ -29,8 +29,7 @@ public class CustomDefaultSnippets { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/CustomEncoding.java b/docs/src/test/java/com/example/restassured/CustomEncoding.java index 6e5c6e867..caa2d6231 100644 --- a/docs/src/test/java/com/example/restassured/CustomEncoding.java +++ b/docs/src/test/java/com/example/restassured/CustomEncoding.java @@ -28,7 +28,7 @@ public class CustomEncoding { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/CustomFormat.java b/docs/src/test/java/com/example/restassured/CustomFormat.java index c0d8adb7e..8f6cf4ab1 100644 --- a/docs/src/test/java/com/example/restassured/CustomFormat.java +++ b/docs/src/test/java/com/example/restassured/CustomFormat.java @@ -29,7 +29,7 @@ public class CustomFormat { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java index 43ef84e79..c31cf228a 100644 --- a/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java +++ b/docs/src/test/java/com/example/restassured/EveryTestPreprocessing.java @@ -38,8 +38,7 @@ public class EveryTestPreprocessing { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); // tag::setup[] private RequestSpecification spec; diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java index aa0a57966..aa91cd4e1 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTestNgTests.java @@ -29,8 +29,7 @@ public class ExampleApplicationTestNgTests { - private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation( - "build/generated-snippets"); + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); @SuppressWarnings("unused") // tag::setup[] diff --git a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java index c7b8d774a..7133906cb 100644 --- a/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java +++ b/docs/src/test/java/com/example/restassured/ExampleApplicationTests.java @@ -28,8 +28,7 @@ public class ExampleApplicationTests { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") // tag::setup[] diff --git a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java index 88db51d8f..e5f2ce475 100644 --- a/docs/src/test/java/com/example/restassured/ParameterizedOutput.java +++ b/docs/src/test/java/com/example/restassured/ParameterizedOutput.java @@ -29,8 +29,7 @@ public class ParameterizedOutput { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @SuppressWarnings("unused") private RequestSpecification spec; diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java index 92ef63746..6eea9876d 100644 --- a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java +++ b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java @@ -42,7 +42,7 @@ public class SampleRestAssuredApplicationTests { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); private RequestSpecification documentationSpec; diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java index d4ea4ca06..61564aca9 100644 --- a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -61,7 +61,7 @@ public class ApiDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private NoteRepository noteRepository; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index 2db8fcb40..82a30e34b 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -61,7 +61,7 @@ public class ApiDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private NoteRepository noteRepository; diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index 29418eb1c..407a8119c 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -58,7 +58,7 @@ public class GettingStartedDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private ObjectMapper objectMapper; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index b3cbb58f5..f63458406 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -70,7 +70,7 @@ public class ApiDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); private RestDocumentationResultHandler documentationHandler; diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index 9531e5831..e147deeb4 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -61,7 +61,7 @@ public class GettingStartedDocumentation { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("build/generated-snippets"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private ObjectMapper objectMapper; diff --git a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java index b49883b25..9c4a9c9b6 100644 --- a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java +++ b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java @@ -39,7 +39,7 @@ @WebAppConfiguration public class SampleTestNgApplicationTests extends AbstractTestNGSpringContextTests { - private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation("build/generated-snippets"); + private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/settings.gradle b/settings.gradle index 8d6619a19..a31b3171b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ rootProject.name = 'spring-restdocs' include 'docs' +include 'spring-restdocs-asciidoctor-extensions' include 'spring-restdocs-core' include 'spring-restdocs-mockmvc' -include 'spring-restdocs-restassured' \ No newline at end of file +include 'spring-restdocs-restassured' diff --git a/spring-restdocs-asciidoctor-extensions/build.gradle b/spring-restdocs-asciidoctor-extensions/build.gradle new file mode 100644 index 000000000..4c24e8b30 --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/build.gradle @@ -0,0 +1,6 @@ +description = 'Spring REST Docs Asciidoctor Extensions' + +dependencies { + compile 'org.asciidoctor:asciidoctorj:1.5.2' + optional 'junit:junit' +} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java new file mode 100644 index 000000000..82a8e60bb --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2016 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.asciidoctor.extensions; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.extension.spi.ExtensionRegistry; + +/** + * ExtensionRegistry for the Spring Rest Docs macros to get registered + * in all projects that include this asciidoctor extension module. + *

          + * Macros provided: + *

            + *
          • {@link RestDocsSnippetBlockMacro}
          • + *
          + * + * @author Gerrit Meier + */ +public class RestDocsExtensionRegistry implements ExtensionRegistry { + + /** + * the name that identifies the block macro in the asciidoctor document + * (e.g.
          snippet::file_to_include[]
          ) + */ + private static final String SNIPPET_BLOCK_NAME = "snippet"; + + @Override + public void register(Asciidoctor asciidoctor) { + asciidoctor.javaExtensionRegistry().blockMacro(SNIPPET_BLOCK_NAME, RestDocsSnippetBlockMacro.class); + } +} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java new file mode 100644 index 000000000..896133f32 --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014-2016 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.asciidoctor.extensions; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Map; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.OptionsBuilder; +import org.asciidoctor.ast.AbstractBlock; +import org.asciidoctor.extension.BlockMacroProcessor; + +/** + * Block macro to include snippets generated by Spring Rest Docs in a convenient way. + * Defaults to build (gradle) or target directory to look for the generated-snippets folder and its content. + * + * @author Gerrit Meier + */ +class RestDocsSnippetBlockMacro extends BlockMacroProcessor { + + private static final String GENERATED_SNIPPETS_PATH = "generated-snippets"; + private static final String MAVEN_TARGET_PATH = "target" + File.separator + GENERATED_SNIPPETS_PATH; + private static final String GRADLE_BUILD_PATH = "build" + File.separator + GENERATED_SNIPPETS_PATH; + private static final String MAVEN_POM = "pom.xml"; + + public RestDocsSnippetBlockMacro(String macroName, Map config) { + super(macroName, config); + } + + @Override + protected Object process(AbstractBlock parent, String fileToInclude, Map attributes) { + String generatedSnippetPath = getDefaultOutputDirectory() + File.separator + fileToInclude; + + // since 'pass' context does not convert the content, we have to do this manually + String convertedContent = Asciidoctor.Factory.create().convertFile( + new File(generatedSnippetPath), + OptionsBuilder.options().toFile(false).inPlace(false).get()); + + return createBlock(parent, "pass", convertedContent, attributes, getConfig()); + } + + private static String getDefaultOutputDirectory() { + String executingDirectory = Paths.get(".").toFile().getAbsolutePath(); + + if (Files.exists(Paths.get(MAVEN_POM))) { + return executingDirectory + File.separator + MAVEN_TARGET_PATH; + } + return executingDirectory + File.separator + GRADLE_BUILD_PATH; + } +} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry b/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry new file mode 100644 index 000000000..084eaa641 --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry @@ -0,0 +1 @@ +org.springframework.restdocs.asciidoctor.extensions.RestDocsExtensionRegistry \ No newline at end of file diff --git a/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java b/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java new file mode 100644 index 000000000..9876af57f --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014-2016 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.asciidoctor.extensions; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Options; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link RestDocsSnippetBlockMacro}. + * + * @author Gerrit Meier + */ +public class RestDocsSnippetBlockMacroTest { + + @Before + public void prepareIncludeFiles() throws Exception { + Files.createDirectories(Paths.get("build/generated-snippets/")); + Files.copy(Paths.get("src/test/resources/rest_docs_macro.adoc"), + Paths.get("build/generated-snippets/rest_docs_macro.adoc"), StandardCopyOption.REPLACE_EXISTING); + } + + @Test + public void replaceRestDocsSnippetBlockWithFile() { + Asciidoctor asciidoctor = Asciidoctor.Factory.create(); + asciidoctor.javaExtensionRegistry().blockMacro("snippet", RestDocsSnippetBlockMacro.class); + + assertThat(asciidoctor.convert("snippet::rest_docs_macro.adoc[]", new Options()), + equalTo("
          \n

          test text

          \n
          ")); + } + +} diff --git a/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc b/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc new file mode 100644 index 000000000..276e7895a --- /dev/null +++ b/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc @@ -0,0 +1 @@ +test text \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java index 635b57f54..8825137e8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/JUnitRestDocumentation.java @@ -32,6 +32,14 @@ public class JUnitRestDocumentation private final ManualRestDocumentation delegate; + /** + * Creates a new {@code JUnitRestDocumentation} instance that will generate snippets + * to <gradle/maven build path>/generated-snippet. + */ + public JUnitRestDocumentation() { + this.delegate = new ManualRestDocumentation(); + } + /** * Creates a new {@code JUnitRestDocumentation} instance that will generate snippets * to the given {@code outputDirectory}. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java index 890d9a4cd..19a8188c5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -17,6 +17,8 @@ package org.springframework.restdocs; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; /** * {@code ManualRestDocumentation} is used to manually manage the @@ -31,10 +33,23 @@ */ public final class ManualRestDocumentation implements RestDocumentationContextProvider { + private static final String GENERATED_SNIPPETS_PATH = "generated-snippets"; + private static final String MAVEN_TARGET_PATH = "target" + File.separator + GENERATED_SNIPPETS_PATH; + private static final String GRADLE_BUILD_PATH = "build" + File.separator + GENERATED_SNIPPETS_PATH; + private static final String MAVEN_POM = "pom.xml"; + private final File outputDirectory; private RestDocumentationContext context; + /** + * Creates a new {@code ManualRestDocumentation} instance that will generate snippets + * to <gradle/maven build path>/generated-snippet. + */ + public ManualRestDocumentation() { + this(getDefaultOutputDirectory()); + } + /** * Creates a new {@code ManualRestDocumentation} instance that will generate snippets * to the given {@code outputDirectory}. @@ -79,4 +94,12 @@ public RestDocumentationContext beforeOperation() { return this.context; } + private static String getDefaultOutputDirectory() { + String executingDirectory = Paths.get(".").toFile().getAbsolutePath(); + + if (Files.exists(Paths.get(MAVEN_POM))) { + return executingDirectory + File.separator + MAVEN_TARGET_PATH; + } + return executingDirectory + File.separator + GRADLE_BUILD_PATH; + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java index 51c96aca2..6018ed692 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/RestDocumentation.java @@ -32,6 +32,14 @@ public class RestDocumentation implements TestRule, RestDocumentationContextProv private final JUnitRestDocumentation delegate; + /** + * Creates a new {@code RestDocumentation} instance that will generate snippets to the + * to <gradle/maven build path>/generated-snippet. + */ + public RestDocumentation() { + this.delegate = new JUnitRestDocumentation(); + } + /** * Creates a new {@code RestDocumentation} instance that will generate snippets to the * given {@code outputDirectory}. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 995a6ffb6..0692cf61c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -195,8 +195,7 @@ public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidocto } private RestDocumentationContext createContext() { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - "build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); manualRestDocumentation.beforeTest(null, null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index 83f96e611..7fa1d1c4d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -83,8 +83,7 @@ private PlaceholderResolver createResolver(String methodName) { } private RestDocumentationContext createContext(String methodName) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - "build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); manualRestDocumentation.beforeTest(getClass(), methodName); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index a4d3460d6..49bdd4ccc 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -70,8 +70,7 @@ public void configuredOutputAndAbsoluteInput() { } private RestDocumentationContext createContext(String outputDir) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - outputDir); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(outputDir); manualRestDocumentation.beforeTest(getClass(), null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 698a80dac..154c59e23 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -123,8 +123,7 @@ public Operation build() { } private RestDocumentationContext createContext() { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( - this.outputDirectory.getAbsolutePath()); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(this.outputDirectory.getAbsolutePath()); manualRestDocumentation.beforeTest(null, null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java index 281c76169..0377ec0bc 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationConfigurerTests.java @@ -44,7 +44,7 @@ public class MockMvcRestDocumentationConfigurerTests { private MockHttpServletRequest request = new MockHttpServletRequest(); @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("test"); + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Test public void defaultConfiguration() { diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 2b2df2e8e..5429591fc 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -101,8 +101,7 @@ public class MockMvcRestDocumentationIntegrationTests { @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Autowired private WebApplicationContext context; diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java index 3417fbbaf..00d889306 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationConfigurerTests.java @@ -47,8 +47,7 @@ public class RestAssuredRestDocumentationConfigurerTests { @Rule - public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build"); + public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); private final FilterableRequestSpecification requestSpec = mock( FilterableRequestSpecification.class); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index f81c91f39..d1089ecc7 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -91,8 +91,7 @@ public class RestAssuredRestDocumentationIntegrationTests { @Rule - public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( - "build/generated-snippets"); + public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @Value("${local.server.port}") private int port; From 319bd139fc620757e122306f0b518ef301ca702d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 11:19:06 +0100 Subject: [PATCH 182/898] Polish contribution and rework to use default attribute rather than macro Rather than introducing a custom macro, this commit opts to implicitly configure the snippets attribute instead. The attribute is configured will the path into which snippets are generated, relative to the directory that contains the Asciidoctor document that is being rendered. The samples and documentation have been updated to use the new spring-restdocs-asciidoctor module and the implicitly configured snippets attribute. Closes gh-297 --- build.gradle | 1 + config/checkstyle/checkstyle-suppressions.xml | 1 - docs/src/docs/asciidoc/getting-started.adoc | 115 +++++++++--------- .../asciidoc/working-with-asciidoctor.adoc | 7 +- samples/rest-assured/build.gradle | 2 +- samples/rest-notes-grails/.gitignore | 5 +- samples/rest-notes-grails/build.gradle | 13 +- .../src/docs/{ => asciidoc}/index.adoc | 0 .../com/example/ApiDocumentationSpec.groovy | 2 +- samples/rest-notes-spring-data-rest/pom.xml | 10 +- .../rest-notes-spring-hateoas/build.gradle | 3 +- samples/testng/build.gradle | 4 +- settings.gradle | 2 +- .../build.gradle | 6 - .../extensions/RestDocsSnippetBlockMacro.java | 66 ---------- ...sciidoctor.extension.spi.ExtensionRegistry | 1 - .../RestDocsSnippetBlockMacroTest.java | 54 -------- .../src/test/resources/rest_docs_macro.adoc | 1 - spring-restdocs-asciidoctor/build.gradle | 7 ++ .../DefaultAttributesPreprocessor.java | 43 +++++++ .../RestDocsExtensionRegistry.java | 24 ++-- .../SnippetsDirectoryResolver.java | 58 +++++++++ ...sciidoctor.extension.spi.ExtensionRegistry | 1 + .../DefaultAttributesPreprocessorTests.java | 58 +++++++++ .../SnippetsDirectoryResolverTests.java | 68 +++++++++++ .../src/test/resources/sample-snippet.adoc | 4 + .../restdocs/ManualRestDocumentation.java | 24 ++-- .../RestDocumentationConfigurerTests.java | 3 +- ...tationContextPlaceholderResolverTests.java | 3 +- .../snippet/StandardWriterResolverTests.java | 3 +- .../restdocs/test/OperationBuilder.java | 3 +- 31 files changed, 348 insertions(+), 244 deletions(-) rename samples/rest-notes-grails/src/docs/{ => asciidoc}/index.adoc (100%) delete mode 100644 spring-restdocs-asciidoctor-extensions/build.gradle delete mode 100644 spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java delete mode 100644 spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry delete mode 100644 spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java delete mode 100644 spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc create mode 100644 spring-restdocs-asciidoctor/build.gradle create mode 100644 spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java rename {spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions => spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor}/RestDocsExtensionRegistry.java (53%) create mode 100644 spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java create mode 100644 spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry create mode 100644 spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java create mode 100644 spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java create mode 100644 spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc diff --git a/build.gradle b/build.gradle index 330e83554..fc664c7e3 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,7 @@ subprojects { dependency 'javax.servlet:javax.servlet-api:3.1.0' dependency 'javax.validation:validation-api:1.1.0.Final' dependency 'junit:junit:4.12' + dependency 'org.asciidoctor:asciidoctorj:1.5.3.1' dependency 'org.hamcrest:hamcrest-core:1.3' dependency 'org.hamcrest:hamcrest-library:1.3' dependency 'org.hibernate:hibernate-validator:5.2.2.Final' diff --git a/config/checkstyle/checkstyle-suppressions.xml b/config/checkstyle/checkstyle-suppressions.xml index 2c9a1795b..f74d92343 100644 --- a/config/checkstyle/checkstyle-suppressions.xml +++ b/config/checkstyle/checkstyle-suppressions.xml @@ -5,5 +5,4 @@ - diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 9b0040ab3..a282bf46f 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -94,13 +94,9 @@ the configuration are described below. test - <2> - ${project.build.directory}/generated-snippets - - - <3> + <2> org.apache.maven.plugins maven-surefire-plugin @@ -109,26 +105,30 @@ the configuration are described below. - <4> + <3> org.asciidoctor asciidoctor-maven-plugin - 1.5.2 + 1.5.3 generate-docs - prepare-package <6> + prepare-package <4> process-asciidoc html book - - ${snippetsDirectory} <5> - + + <5> + org.springframework.restdocs + spring-restdocs-asciidoctor + {project-version} + + @@ -136,49 +136,50 @@ the configuration are described below. <1> Add a dependency on `spring-restdocs-mockmvc` in the `test` scope. If you want to use REST Assured rather than MockMvc, add a dependency on `spring-restdocs-restassured` instead. -<2> Configure a property to define the output location for generated snippets. -<3> Add the SureFire plugin and configure it to include files whose names end with +<2> Add the SureFire plugin and configure it to include files whose names end with `Documentation.java`. -<4> Add the Asciidoctor plugin -<5> Define an attribute named `snippets` that can be used when including the generated - snippets in your documentation. -<6> Using `prepare-package` allows the documentation to be +<3> Add the Asciidoctor plugin. +<4> Using `prepare-package` allows the documentation to be <>. +<5> Add `spring-restdocs-asciidoctor` as a dependency of the Asciidoctor plugin. This + will automatically configure the `snippets` attribute for use in your `.adoc` files to + point to `target/generated-snippets`. [source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] .Gradle ---- plugins { <1> - id "org.asciidoctor.convert" version "1.5.2" + id "org.asciidoctor.convert" version "1.5.3" } - dependencies { <2> - testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' + dependencies { + asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' <2> + testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' <3> } - ext { <3> + ext { <4> snippetsDir = file('build/generated-snippets') } - test { <4> + test { <5> outputs.dir snippetsDir } - asciidoctor { <5> - attributes 'snippets': snippetsDir <6> + asciidoctor { <6> inputs.dir snippetsDir <7> dependsOn test <8> } ---- <1> Apply the Asciidoctor plugin. -<2> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. If +<2> Add a dependency on `spring-restdocs-asciidoctor` in the `asciidoctor` configuration. + This will automatically configure the `snippets` attribute for use in your `.adoc` + files to point to `build/generated-snippets`. +<3> Add a dependency on `spring-restdocs-mockmvc` in the `testCompile` configuration. If you want to use REST Assured rather than MockMvc, add a dependency on `spring-restdocs-restassured` instead. -<3> Configure a property to define the output location for generated snippets. -<4> Configure the `test` task to add the snippets directory as an output. -<5> Configure the `asciidoctor` task -<6> Define an attribute named `snippets` that can be used when including the generated - snippets in your documentation. +<4> Configure a property to define the output location for generated snippets. +<5> Configure the `test` task to add the snippets directory as an output. +<6> Configure the `asciidoctor` task <7> Configure the snippets directory as an input. <8> Make the task depend on the test task so that the tests are run before the documentation is created. @@ -271,28 +272,38 @@ are also supported although slightly more setup is required. ===== Setting up your JUnit tests When using JUnit, the first step in generating documentation snippets is to declare a -`public` `JUnitRestDocumentation` field that's annotated as a JUnit `@Rule`. The -`JUnitRestDocumentation` rule is configured with the output directory into which generated -snippets should be written. This output directory should match the snippets directory that -you have configured in your `build.gradle` or `pom.xml` file. +`public` `JUnitRestDocumentation` field that's annotated as a JUnit `@Rule`. -For Maven (`pom.xml`) that will typically be `target/generated-snippets` and for -Gradle (`build.gradle`) it will typically be `build/generated-snippets`: -[source,java,indent=0,role="primary"] -.Maven +[source,java,indent=0] ---- @Rule -public JUnitRestDocumentation restDocumentation = - new JUnitRestDocumentation("target/generated-snippets"); +public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); ---- -[source,java,indent=0,role="secondary"] -.Gradle + +By default, the `JUnitRestDocumentation` rule is automatically configured with an output +directory based on your project's build tool: + +[cols="2,5"] +|=== +| Build tool | Output directory + +| Maven +| `target/generated-snippets` + +| Gradle +| `build/generated-snippets` + +|=== + +The default can be overridden by providing an output directory when creating the +`JUnitRestDocumentation` instance: + +[source,java,indent=0] ---- @Rule -public JUnitRestDocumentation restDocumentation = - new JUnitRestDocumentation("build/generated-snippets"); +public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("custom"); ---- Next, provide an `@Before` method to configure MockMvc or REST Assured: @@ -331,18 +342,9 @@ illustrates the approach. The first difference is that `ManualRestDocumentation` should be used in place of `JUnitRestDocumentation` and there's no need for the `@Rule` annotation: -[source,java,indent=0,role="primary"] -.Maven ----- -private ManualRestDocumentation restDocumentation = - new ManualRestDocumentation("target/generated-snippets"); ----- - -[source,java,indent=0,role="secondary"] -.Gradle +[source,java,indent=0] ---- -private ManualRestDocumentation restDocumentation = - new ManualRestDocumentation("build/generated-snippets"); +private ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); ---- Secondly, `ManualRestDocumentation.beforeTest(Class, String)` @@ -441,7 +443,8 @@ the resulting HTML files depends on whether you are using Maven or Gradle: The generated snippets can then be included in the manually created Asciidoctor file from above using the http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro]. -The `snippets` attribute specified in the <> can be used to reference the snippets output directory. For example: [source,adoc,indent=0] diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index b3981dfeb..283afcdf7 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -17,10 +17,11 @@ relevant to Spring REST Docs. [[working-with-asciidoctor-including-snippets]] === Including snippets -The http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include +The http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#include-files[include macro] is used to include generated snippets in your documentation. The `snippets` -attribute specified in the <> -can be used to reference the snippets output directory, for example: +attribute that is automatically set by `spring-restdocs-asciidoctor` configured in the +<> can be used to reference the +snippets output directory. For example: [source,adoc,indent=0] ---- diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index ea67c8610..b8776d376 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -37,6 +37,7 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-web' testCompile 'org.springframework.boot:spring-boot-starter-test' testCompile "org.springframework.restdocs:spring-restdocs-restassured:${project.ext['spring-restdocs.version']}" + asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}" } test { @@ -44,7 +45,6 @@ test { } asciidoctor { - attributes 'snippets': snippetsDir inputs.dir snippetsDir dependsOn test } diff --git a/samples/rest-notes-grails/.gitignore b/samples/rest-notes-grails/.gitignore index a521ca89f..18afeb782 100644 --- a/samples/rest-notes-grails/.gitignore +++ b/samples/rest-notes-grails/.gitignore @@ -11,7 +11,4 @@ classes/ .settings .classpath gradlew* -gradle/wrapper - - -src/docs/generated-snippets +gradle/wrapper \ No newline at end of file diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 52ef83950..64e2e6d8e 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -44,6 +44,7 @@ repositories { dependencyManagement { dependencies { dependency "org.springframework.restdocs:spring-restdocs-restassured:$restDocsVersion" + dependency "org.springframework.restdocs:spring-restdocs-asciidoctor:$restDocsVersion" } imports { mavenBom "org.grails:grails-bom:$grailsVersion" @@ -53,6 +54,8 @@ dependencyManagement { } dependencies { + asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor" + compile "org.springframework.boot:spring-boot-starter-logging" compile "org.springframework.boot:spring-boot-starter-actuator" compile "org.springframework.boot:spring-boot-autoconfigure" @@ -85,24 +88,16 @@ task wrapper(type: Wrapper) { } ext { - snippetsDir = file('src/docs/generated-snippets') -} - -task cleanTempDirs(type: Delete) { - delete fileTree(dir: 'src/docs/generated-snippets') + snippetsDir = file('build/generated-snippets') } test { - dependsOn cleanTempDirs outputs.dir snippetsDir } asciidoctor { dependsOn integrationTest inputs.dir snippetsDir - sourceDir = file('src/docs') - separateOutputDirs = false - attributes 'snippets': snippetsDir } build.dependsOn asciidoctor diff --git a/samples/rest-notes-grails/src/docs/index.adoc b/samples/rest-notes-grails/src/docs/asciidoc/index.adoc similarity index 100% rename from samples/rest-notes-grails/src/docs/index.adoc rename to samples/rest-notes-grails/src/docs/asciidoc/index.adoc diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index 3f0da9cae..2c014fc01 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -45,7 +45,7 @@ import spock.lang.Specification class ApiDocumentationSpec extends Specification { @Rule - JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation('src/docs/generated-snippets') + JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation() @Value('${local.server.port}') Integer serverPort diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index cae3b9a8a..022c19226 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -84,12 +84,16 @@ html book - - ${project.build.directory}/generated-snippets - + + + org.springframework.restdocs + spring-restdocs-asciidoctor + ${spring-restdocs.version} + + maven-resources-plugin diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index 89a457416..a44f3f942 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -34,6 +34,8 @@ ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { + asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}" + compile 'org.springframework.boot:spring-boot-starter-data-jpa' compile 'org.springframework.boot:spring-boot-starter-hateoas' @@ -50,7 +52,6 @@ test { } asciidoctor { - attributes 'snippets': snippetsDir inputs.dir snippetsDir dependsOn test } diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index b0be86971..3be74243e 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -34,7 +34,10 @@ ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { + asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}" + compile 'org.springframework.boot:spring-boot-starter-web' + testCompile('org.springframework:spring-test') { exclude group: 'junit', module: 'junit;' } @@ -48,7 +51,6 @@ test { } asciidoctor { - attributes 'snippets': snippetsDir inputs.dir snippetsDir dependsOn test } diff --git a/settings.gradle b/settings.gradle index a31b3171b..94b5f9b14 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,7 @@ rootProject.name = 'spring-restdocs' include 'docs' -include 'spring-restdocs-asciidoctor-extensions' +include 'spring-restdocs-asciidoctor' include 'spring-restdocs-core' include 'spring-restdocs-mockmvc' include 'spring-restdocs-restassured' diff --git a/spring-restdocs-asciidoctor-extensions/build.gradle b/spring-restdocs-asciidoctor-extensions/build.gradle deleted file mode 100644 index 4c24e8b30..000000000 --- a/spring-restdocs-asciidoctor-extensions/build.gradle +++ /dev/null @@ -1,6 +0,0 @@ -description = 'Spring REST Docs Asciidoctor Extensions' - -dependencies { - compile 'org.asciidoctor:asciidoctorj:1.5.2' - optional 'junit:junit' -} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java b/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java deleted file mode 100644 index 896133f32..000000000 --- a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacro.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2014-2016 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.asciidoctor.extensions; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Map; - -import org.asciidoctor.Asciidoctor; -import org.asciidoctor.OptionsBuilder; -import org.asciidoctor.ast.AbstractBlock; -import org.asciidoctor.extension.BlockMacroProcessor; - -/** - * Block macro to include snippets generated by Spring Rest Docs in a convenient way. - * Defaults to build (gradle) or target directory to look for the generated-snippets folder and its content. - * - * @author Gerrit Meier - */ -class RestDocsSnippetBlockMacro extends BlockMacroProcessor { - - private static final String GENERATED_SNIPPETS_PATH = "generated-snippets"; - private static final String MAVEN_TARGET_PATH = "target" + File.separator + GENERATED_SNIPPETS_PATH; - private static final String GRADLE_BUILD_PATH = "build" + File.separator + GENERATED_SNIPPETS_PATH; - private static final String MAVEN_POM = "pom.xml"; - - public RestDocsSnippetBlockMacro(String macroName, Map config) { - super(macroName, config); - } - - @Override - protected Object process(AbstractBlock parent, String fileToInclude, Map attributes) { - String generatedSnippetPath = getDefaultOutputDirectory() + File.separator + fileToInclude; - - // since 'pass' context does not convert the content, we have to do this manually - String convertedContent = Asciidoctor.Factory.create().convertFile( - new File(generatedSnippetPath), - OptionsBuilder.options().toFile(false).inPlace(false).get()); - - return createBlock(parent, "pass", convertedContent, attributes, getConfig()); - } - - private static String getDefaultOutputDirectory() { - String executingDirectory = Paths.get(".").toFile().getAbsolutePath(); - - if (Files.exists(Paths.get(MAVEN_POM))) { - return executingDirectory + File.separator + MAVEN_TARGET_PATH; - } - return executingDirectory + File.separator + GRADLE_BUILD_PATH; - } -} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry b/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry deleted file mode 100644 index 084eaa641..000000000 --- a/spring-restdocs-asciidoctor-extensions/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry +++ /dev/null @@ -1 +0,0 @@ -org.springframework.restdocs.asciidoctor.extensions.RestDocsExtensionRegistry \ No newline at end of file diff --git a/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java b/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java deleted file mode 100644 index 9876af57f..000000000 --- a/spring-restdocs-asciidoctor-extensions/src/test/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsSnippetBlockMacroTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2014-2016 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.asciidoctor.extensions; - -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; - -import org.asciidoctor.Asciidoctor; -import org.asciidoctor.Options; -import org.junit.Before; -import org.junit.Test; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.junit.Assert.assertThat; - -/** - * Tests for {@link RestDocsSnippetBlockMacro}. - * - * @author Gerrit Meier - */ -public class RestDocsSnippetBlockMacroTest { - - @Before - public void prepareIncludeFiles() throws Exception { - Files.createDirectories(Paths.get("build/generated-snippets/")); - Files.copy(Paths.get("src/test/resources/rest_docs_macro.adoc"), - Paths.get("build/generated-snippets/rest_docs_macro.adoc"), StandardCopyOption.REPLACE_EXISTING); - } - - @Test - public void replaceRestDocsSnippetBlockWithFile() { - Asciidoctor asciidoctor = Asciidoctor.Factory.create(); - asciidoctor.javaExtensionRegistry().blockMacro("snippet", RestDocsSnippetBlockMacro.class); - - assertThat(asciidoctor.convert("snippet::rest_docs_macro.adoc[]", new Options()), - equalTo("
          \n

          test text

          \n
          ")); - } - -} diff --git a/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc b/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc deleted file mode 100644 index 276e7895a..000000000 --- a/spring-restdocs-asciidoctor-extensions/src/test/resources/rest_docs_macro.adoc +++ /dev/null @@ -1 +0,0 @@ -test text \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/build.gradle b/spring-restdocs-asciidoctor/build.gradle new file mode 100644 index 000000000..6897242eb --- /dev/null +++ b/spring-restdocs-asciidoctor/build.gradle @@ -0,0 +1,7 @@ +description = 'Asciidoctor extensions for Spring REST Docs' + +dependencies { + compileOnly 'org.asciidoctor:asciidoctorj' + testCompile 'junit:junit' + testCompile 'org.asciidoctor:asciidoctorj' +} diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java new file mode 100644 index 000000000..7caa35f9c --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2016 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.asciidoctor; + +import java.io.File; + +import org.asciidoctor.ast.Document; +import org.asciidoctor.extension.Preprocessor; +import org.asciidoctor.extension.PreprocessorReader; + +/** + * {@link Preprocessor} that sets defaults for REST Docs-related {@link Document} + * attributes. + * + * @author Andy Wilkinson + */ +final class DefaultAttributesPreprocessor extends Preprocessor { + + private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver( + new File(".")); + + @Override + public PreprocessorReader process(Document document, PreprocessorReader reader) { + document.setAttr("snippets", this.snippetsDirectoryResolver + .getSnippetsDirectory(document.getAttributes()), false); + return reader; + } + +} diff --git a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java similarity index 53% rename from spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java rename to spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java index 82a8e60bb..15bca1542 100644 --- a/spring-restdocs-asciidoctor-extensions/src/main/java/org/springframework/restdocs/asciidoctor/extensions/RestDocsExtensionRegistry.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/RestDocsExtensionRegistry.java @@ -14,32 +14,22 @@ * limitations under the License. */ -package org.springframework.restdocs.asciidoctor.extensions; +package org.springframework.restdocs.asciidoctor; import org.asciidoctor.Asciidoctor; import org.asciidoctor.extension.spi.ExtensionRegistry; /** - * ExtensionRegistry for the Spring Rest Docs macros to get registered - * in all projects that include this asciidoctor extension module. - *

          - * Macros provided: - *

            - *
          • {@link RestDocsSnippetBlockMacro}
          • - *
          + * Asciidoctor {@link ExtensionRegistry} for Spring REST Docs. * - * @author Gerrit Meier + * @author Andy Wilkinson */ -public class RestDocsExtensionRegistry implements ExtensionRegistry { - - /** - * the name that identifies the block macro in the asciidoctor document - * (e.g.
          snippet::file_to_include[]
          ) - */ - private static final String SNIPPET_BLOCK_NAME = "snippet"; +public final class RestDocsExtensionRegistry implements ExtensionRegistry { @Override public void register(Asciidoctor asciidoctor) { - asciidoctor.javaExtensionRegistry().blockMacro(SNIPPET_BLOCK_NAME, RestDocsSnippetBlockMacro.class); + asciidoctor.javaExtensionRegistry() + .preprocessor(new DefaultAttributesPreprocessor()); } + } diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java new file mode 100644 index 000000000..dd6829ade --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2016 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.asciidoctor; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +/** + * Resolves the directory from which snippets can be read for inclusion in an Asciidoctor + * document. The resolved directory is relative to the {@code docdir} of the Asciidoctor + * document that it being rendered. + * + * @author Andy Wilkinson + */ +class SnippetsDirectoryResolver { + + private final File root; + + SnippetsDirectoryResolver(File root) { + this.root = root; + } + + File getSnippetsDirectory(Map attributes) { + if (new File(this.root, "pom.xml").exists()) { + return getMavenSnippetsDirectory(attributes); + } + return getGradleSnippetsDirectory(attributes); + } + + private File getMavenSnippetsDirectory(Map attributes) { + Path rootPath = Paths.get(this.root.getAbsolutePath()); + Path docDirPath = Paths.get((String) attributes.get("docdir")); + Path relativePath = docDirPath.relativize(rootPath); + return new File(relativePath.toFile(), "target/generated-snippets"); + } + + private File getGradleSnippetsDirectory(Map attributes) { + return new File((String) attributes.get("projectdir"), + "build/generated-snippets"); + } + +} diff --git a/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry b/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry new file mode 100644 index 000000000..7c790a308 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/resources/META-INF/services/org.asciidoctor.extension.spi.ExtensionRegistry @@ -0,0 +1 @@ +org.springframework.restdocs.asciidoctor.RestDocsExtensionRegistry \ No newline at end of file diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java new file mode 100644 index 000000000..699bb49a6 --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014-2016 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.asciidoctor; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Attributes; +import org.asciidoctor.Options; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link DefaultAttributesPreprocessor}. + * + * @author Andy Wilkinson + */ +public class DefaultAttributesPreprocessorTests { + + @Test + public void snippetsAttributeIsSet() { + String converted = Asciidoctor.Factory.create().convert("{snippets}", + new Options()); + assertThat(converted, containsString("build/generated-snippets")); + } + + @Test + public void snippetsAttributeFromConvertArgumentIsNotOverridden() { + Options options = new Options(); + options.setAttributes(new Attributes("snippets=custom")); + String converted = Asciidoctor.Factory.create().convert("{snippets}", options); + assertThat(converted, containsString("custom")); + } + + @Test + public void snippetsAttributeFromDocumentPreambleIsNotOverridden() { + Options options = new Options(); + options.setAttributes(new Attributes("snippets=custom")); + String converted = Asciidoctor.Factory.create() + .convert(":snippets: custom\n{snippets}", options); + assertThat(converted, containsString("custom")); + } + +} diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java new file mode 100644 index 000000000..f998caf4d --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2014-2016 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.asciidoctor; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link SnippetsDirectoryResolver}. + * + * @author Andy Wilkinson + */ +public class SnippetsDirectoryResolverTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void mavenProjectsUseTargetGeneratedSnippetsRelativeToDocDir() + throws IOException { + this.temporaryFolder.newFile("pom.xml"); + Map attributes = new HashMap<>(); + attributes.put("docdir", + new File(this.temporaryFolder.getRoot(), "src/main/asciidoc") + .getAbsolutePath()); + File snippetsDirectory = new SnippetsDirectoryResolver( + this.temporaryFolder.getRoot()).getSnippetsDirectory(attributes); + assertThat(snippetsDirectory.isAbsolute(), is(false)); + assertThat(snippetsDirectory, + equalTo(new File("../../../target/generated-snippets"))); + } + + @Test + public void gradleProjectsUseBuildGeneratedSnippetsBeneathProjectDir() + throws IOException { + Map attributes = new HashMap<>(); + attributes.put("projectdir", "project/dir"); + File snippetsDirectory = new SnippetsDirectoryResolver( + this.temporaryFolder.getRoot()).getSnippetsDirectory(attributes); + assertThat(snippetsDirectory, + equalTo(new File("project/dir/build/generated-snippets"))); + } + +} diff --git a/spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc b/spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc new file mode 100644 index 000000000..0a3d1fa7e --- /dev/null +++ b/spring-restdocs-asciidoctor/src/test/resources/sample-snippet.adoc @@ -0,0 +1,4 @@ +[source,bash] +---- +$ curl 'http://localhost:8080/' -i -H 'Accept: application/hal+json' +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java index 19a8188c5..472832ff6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/ManualRestDocumentation.java @@ -17,8 +17,6 @@ package org.springframework.restdocs; import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; /** * {@code ManualRestDocumentation} is used to manually manage the @@ -33,11 +31,6 @@ */ public final class ManualRestDocumentation implements RestDocumentationContextProvider { - private static final String GENERATED_SNIPPETS_PATH = "generated-snippets"; - private static final String MAVEN_TARGET_PATH = "target" + File.separator + GENERATED_SNIPPETS_PATH; - private static final String GRADLE_BUILD_PATH = "build" + File.separator + GENERATED_SNIPPETS_PATH; - private static final String MAVEN_POM = "pom.xml"; - private final File outputDirectory; private RestDocumentationContext context; @@ -57,7 +50,11 @@ public ManualRestDocumentation() { * @param outputDirectory the output directory */ public ManualRestDocumentation(String outputDirectory) { - this.outputDirectory = new File(outputDirectory); + this(new File(outputDirectory)); + } + + private ManualRestDocumentation(File outputDirectory) { + this.outputDirectory = outputDirectory; } /** @@ -94,12 +91,11 @@ public RestDocumentationContext beforeOperation() { return this.context; } - private static String getDefaultOutputDirectory() { - String executingDirectory = Paths.get(".").toFile().getAbsolutePath(); - - if (Files.exists(Paths.get(MAVEN_POM))) { - return executingDirectory + File.separator + MAVEN_TARGET_PATH; + private static File getDefaultOutputDirectory() { + if (new File("pom.xml").exists()) { + return new File("target/generated-snippets"); } - return executingDirectory + File.separator + GRADLE_BUILD_PATH; + return new File("build/generated-snippets"); } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 0692cf61c..995a6ffb6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -195,7 +195,8 @@ public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidocto } private RestDocumentationContext createContext() { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + "build"); manualRestDocumentation.beforeTest(null, null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java index 7fa1d1c4d..83f96e611 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/RestDocumentationContextPlaceholderResolverTests.java @@ -83,7 +83,8 @@ private PlaceholderResolver createResolver(String methodName) { } private RestDocumentationContext createContext(String methodName) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation("build"); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + "build"); manualRestDocumentation.beforeTest(getClass(), methodName); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java index 49bdd4ccc..a4d3460d6 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/StandardWriterResolverTests.java @@ -70,7 +70,8 @@ public void configuredOutputAndAbsoluteInput() { } private RestDocumentationContext createContext(String outputDir) { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(outputDir); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + outputDir); manualRestDocumentation.beforeTest(getClass(), null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 154c59e23..698a80dac 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -123,7 +123,8 @@ public Operation build() { } private RestDocumentationContext createContext() { - ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(this.outputDirectory.getAbsolutePath()); + ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation( + this.outputDirectory.getAbsolutePath()); manualRestDocumentation.beforeTest(null, null); RestDocumentationContext context = manualRestDocumentation.beforeOperation(); return context; From 0c64c781f49e154c9dfaa76bd5569fad16154304 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 23:09:47 +0100 Subject: [PATCH 183/898] Improve styling of groovy and adoc code blocks in the documentation Closes gh-317 --- docs/src/docs/asciidoc/getting-started.adoc | 4 ++-- docs/src/docs/asciidoc/working-with-asciidoctor.adoc | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index 0dd076206..8c5272998 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -145,7 +145,7 @@ the configuration are described below. <6> Using `prepare-package` allows the documentation to be <>. -[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +[source,indent=0,subs="verbatim,attributes",role="secondary"] .Gradle ---- plugins { <1> @@ -235,7 +235,7 @@ Asciidoctor plugin to ensure that the documentation is generated before it's cop <3> Copy the generated documentation into the build output's `static/docs` directory, from where it will be included in the jar file. -[source,groovy,indent=0,role="secondary"] +[source,indent=0,role="secondary"] .Gradle ---- jar { diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index b3981dfeb..5ed3e9c09 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -22,7 +22,7 @@ macro] is used to include generated snippets in your documentation. The `snippet attribute specified in the <> can be used to reference the snippets output directory, for example: -[source,adoc,indent=0] +[source,indent=0] ---- \include::{snippets}/index/curl-request.adoc[] ---- @@ -45,7 +45,7 @@ Asciidoctor has rich support for http://asciidoctor.org/docs/user-manual/#cols-format[formatting a table's columns]. For example, the widths of a table's columns can be specified using the `cols` attribute: -[source,adoc,indent=0] +[source,indent=0] ---- [cols=1,3] <1> \include::{snippets}/index/links.adoc[] @@ -60,7 +60,7 @@ three times as wide as the first. The title of a table can be specified using a line prefixed by a `.`: -[source,adoc,indent=0] +[source,indent=0] ---- .Links <1> \include::{snippets}/index/links.adoc[] From a2f1391fc88ed1aca77b06b56c51e75147294e70 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 23:10:47 +0100 Subject: [PATCH 184/898] Merge branch '1.1.x' --- docs/src/docs/asciidoc/getting-started.adoc | 4 ++-- docs/src/docs/asciidoc/working-with-asciidoctor.adoc | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index a282bf46f..d70a78ce7 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -145,7 +145,7 @@ the configuration are described below. will automatically configure the `snippets` attribute for use in your `.adoc` files to point to `target/generated-snippets`. -[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +[source,indent=0,subs="verbatim,attributes",role="secondary"] .Gradle ---- plugins { <1> @@ -236,7 +236,7 @@ Asciidoctor plugin to ensure that the documentation is generated before it's cop <3> Copy the generated documentation into the build output's `static/docs` directory, from where it will be included in the jar file. -[source,groovy,indent=0,role="secondary"] +[source,indent=0,role="secondary"] .Gradle ---- jar { diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index 283afcdf7..008ab6f98 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -23,7 +23,7 @@ attribute that is automatically set by `spring-restdocs-asciidoctor` configured <> can be used to reference the snippets output directory. For example: -[source,adoc,indent=0] +[source,indent=0] ---- \include::{snippets}/index/curl-request.adoc[] ---- @@ -46,7 +46,7 @@ Asciidoctor has rich support for http://asciidoctor.org/docs/user-manual/#cols-format[formatting a table's columns]. For example, the widths of a table's columns can be specified using the `cols` attribute: -[source,adoc,indent=0] +[source,indent=0] ---- [cols=1,3] <1> \include::{snippets}/index/links.adoc[] @@ -61,7 +61,7 @@ three times as wide as the first. The title of a table can be specified using a line prefixed by a `.`: -[source,adoc,indent=0] +[source,indent=0] ---- .Links <1> \include::{snippets}/index/links.adoc[] From 2f568c21212a1b68cbc9825260f261c54eaca62b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 23:12:11 +0100 Subject: [PATCH 185/898] Install spring-restdocs-asciidoctor before building samples --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index fc664c7e3..7750213db 100644 --- a/build.gradle +++ b/build.gradle @@ -163,6 +163,7 @@ samples { dependOn 'spring-restdocs-core:install' dependOn 'spring-restdocs-mockmvc:install' dependOn 'spring-restdocs-restassured:install' + dependOn 'spring-restdocs-asciidoctor:install' restNotesGrails { workingDir "$projectDir/samples/rest-notes-grails" From c13b7d03f5f6fa2d1b21e5ca6ff095a20b525415 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Oct 2016 23:26:18 +0100 Subject: [PATCH 186/898] Fix DefaultAttributesPreprocessorTests on Windows --- .../asciidoctor/DefaultAttributesPreprocessorTests.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java index 699bb49a6..8903c99e9 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -16,6 +16,8 @@ package org.springframework.restdocs.asciidoctor; +import java.io.File; + import org.asciidoctor.Asciidoctor; import org.asciidoctor.Attributes; import org.asciidoctor.Options; @@ -35,7 +37,8 @@ public class DefaultAttributesPreprocessorTests { public void snippetsAttributeIsSet() { String converted = Asciidoctor.Factory.create().convert("{snippets}", new Options()); - assertThat(converted, containsString("build/generated-snippets")); + assertThat(converted, + containsString("build" + File.separatorChar + "generated-snippets")); } @Test From 49596df0f3b59cd22329f70ff0b7f07ebc3ebf07 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 22 Oct 2016 21:58:11 +0100 Subject: [PATCH 187/898] Make snippet directory resolution more robust The previous approach had (at least) two problems: - A Gradle build run from a directory that also contains a pom.xml would result in the resolver incorrectly identifing that Maven was being used - A Maven build run from a directory that did not could a pom and that used -f to provide the path to a pom would result in the resolver incorretly indentifying that Gradle was being used With this commit, the resolver now uses the presence of the maven.home system property to identify that Maven is being used. When Maven is being used, rather than looking for a pom.xml in the working directory, the resolver now locates the pom.xml by searching up the directory hierarchy from the docdir. Closes gh-297 --- .../DefaultAttributesPreprocessor.java | 5 +- .../SnippetsDirectoryResolver.java | 38 ++++++++----- .../DefaultAttributesPreprocessorTests.java | 9 ++-- .../SnippetsDirectoryResolverTests.java | 53 +++++++++++++++++-- 4 files changed, 80 insertions(+), 25 deletions(-) diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java index 7caa35f9c..12ffd02ae 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessor.java @@ -16,8 +16,6 @@ package org.springframework.restdocs.asciidoctor; -import java.io.File; - import org.asciidoctor.ast.Document; import org.asciidoctor.extension.Preprocessor; import org.asciidoctor.extension.PreprocessorReader; @@ -30,8 +28,7 @@ */ final class DefaultAttributesPreprocessor extends Preprocessor { - private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver( - new File(".")); + private final SnippetsDirectoryResolver snippetsDirectoryResolver = new SnippetsDirectoryResolver(); @Override public PreprocessorReader process(Document document, PreprocessorReader reader) { diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java index dd6829ade..4e9c53eaf 100644 --- a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolver.java @@ -17,6 +17,7 @@ package org.springframework.restdocs.asciidoctor; import java.io.File; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; @@ -30,29 +31,42 @@ */ class SnippetsDirectoryResolver { - private final File root; - - SnippetsDirectoryResolver(File root) { - this.root = root; - } - File getSnippetsDirectory(Map attributes) { - if (new File(this.root, "pom.xml").exists()) { + if (System.getProperty("maven.home") != null) { return getMavenSnippetsDirectory(attributes); } return getGradleSnippetsDirectory(attributes); } private File getMavenSnippetsDirectory(Map attributes) { - Path rootPath = Paths.get(this.root.getAbsolutePath()); - Path docDirPath = Paths.get((String) attributes.get("docdir")); - Path relativePath = docDirPath.relativize(rootPath); - return new File(relativePath.toFile(), "target/generated-snippets"); + Path docdir = Paths.get(getRequiredAttribute(attributes, "docdir")); + return new File(docdir.relativize(findPom(docdir).getParent()).toFile(), + "target/generated-snippets"); + } + + private Path findPom(Path docdir) { + Path path = docdir; + while (path != null) { + Path pom = path.resolve("pom.xml"); + if (Files.isRegularFile(pom)) { + return pom; + } + path = path.getParent(); + } + throw new IllegalStateException("pom.xml not found in '" + docdir + "' or above"); } private File getGradleSnippetsDirectory(Map attributes) { - return new File((String) attributes.get("projectdir"), + return new File(getRequiredAttribute(attributes, "projectdir"), "build/generated-snippets"); } + private String getRequiredAttribute(Map attributes, String name) { + String attribute = (String) attributes.get(name); + if (attribute == null || attribute.length() == 0) { + throw new IllegalStateException(name + " attribute not found"); + } + return attribute; + } + } diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java index 8903c99e9..83ef47824 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/DefaultAttributesPreprocessorTests.java @@ -35,8 +35,9 @@ public class DefaultAttributesPreprocessorTests { @Test public void snippetsAttributeIsSet() { - String converted = Asciidoctor.Factory.create().convert("{snippets}", - new Options()); + Options options = new Options(); + options.setAttributes(new Attributes("projectdir=../../..")); + String converted = Asciidoctor.Factory.create().convert("{snippets}", options); assertThat(converted, containsString("build" + File.separatorChar + "generated-snippets")); } @@ -44,7 +45,7 @@ public void snippetsAttributeIsSet() { @Test public void snippetsAttributeFromConvertArgumentIsNotOverridden() { Options options = new Options(); - options.setAttributes(new Attributes("snippets=custom")); + options.setAttributes(new Attributes("snippets=custom projectdir=../../..")); String converted = Asciidoctor.Factory.create().convert("{snippets}", options); assertThat(converted, containsString("custom")); } @@ -52,7 +53,7 @@ public void snippetsAttributeFromConvertArgumentIsNotOverridden() { @Test public void snippetsAttributeFromDocumentPreambleIsNotOverridden() { Options options = new Options(); - options.setAttributes(new Attributes("snippets=custom")); + options.setAttributes(new Attributes("projectdir=../../..")); String converted = Asciidoctor.Factory.create() .convert(":snippets: custom\n{snippets}", options); assertThat(converted, containsString("custom")); diff --git a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java index f998caf4d..74bbda7e7 100644 --- a/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java +++ b/spring-restdocs-asciidoctor/src/test/java/org/springframework/restdocs/asciidoctor/SnippetsDirectoryResolverTests.java @@ -23,6 +23,7 @@ import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import static org.hamcrest.CoreMatchers.equalTo; @@ -39,30 +40,72 @@ public class SnippetsDirectoryResolverTests { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + @Rule + public ExpectedException thrown = ExpectedException.none(); + @Test - public void mavenProjectsUseTargetGeneratedSnippetsRelativeToDocDir() + public void mavenProjectsUseTargetGeneratedSnippetsRelativeToDocdir() throws IOException { this.temporaryFolder.newFile("pom.xml"); Map attributes = new HashMap<>(); attributes.put("docdir", new File(this.temporaryFolder.getRoot(), "src/main/asciidoc") .getAbsolutePath()); - File snippetsDirectory = new SnippetsDirectoryResolver( - this.temporaryFolder.getRoot()).getSnippetsDirectory(attributes); + File snippetsDirectory = getMavenSnippetsDirectory(attributes); assertThat(snippetsDirectory.isAbsolute(), is(false)); assertThat(snippetsDirectory, equalTo(new File("../../../target/generated-snippets"))); } + @Test + public void illegalStateExceptionWhenMavenPomCannotBeFound() throws IOException { + Map attributes = new HashMap<>(); + String docdir = new File(this.temporaryFolder.getRoot(), "src/main/asciidoc") + .getAbsolutePath(); + attributes.put("docdir", docdir); + this.thrown.expect(IllegalStateException.class); + this.thrown + .expectMessage(equalTo("pom.xml not found in '" + docdir + "' or above")); + getMavenSnippetsDirectory(attributes); + } + + @Test + public void illegalStateWhenDocdirAttributeIsNotSetInMavenProject() + throws IOException { + Map attributes = new HashMap<>(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage(equalTo("docdir attribute not found")); + getMavenSnippetsDirectory(attributes); + } + @Test public void gradleProjectsUseBuildGeneratedSnippetsBeneathProjectDir() throws IOException { Map attributes = new HashMap<>(); attributes.put("projectdir", "project/dir"); - File snippetsDirectory = new SnippetsDirectoryResolver( - this.temporaryFolder.getRoot()).getSnippetsDirectory(attributes); + File snippetsDirectory = new SnippetsDirectoryResolver() + .getSnippetsDirectory(attributes); assertThat(snippetsDirectory, equalTo(new File("project/dir/build/generated-snippets"))); } + @Test + public void illegalStateWhenProjectdirAttributeIsNotSetInGradleProject() + throws IOException { + Map attributes = new HashMap<>(); + this.thrown.expect(IllegalStateException.class); + this.thrown.expectMessage(equalTo("projectdir attribute not found")); + new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); + } + + private File getMavenSnippetsDirectory(Map attributes) { + System.setProperty("maven.home", "/maven/home"); + try { + return new SnippetsDirectoryResolver().getSnippetsDirectory(attributes); + } + finally { + System.clearProperty("maven.home"); + } + } + } From 27b75538125f967098be5fd58f2fc9dd3ecd13ff Mon Sep 17 00:00:00 2001 From: Luis del Toro Date: Sat, 15 Oct 2016 16:33:14 +0200 Subject: [PATCH 188/898] Copy attributes when applying path prefix to field descriptors See gh-314 --- .../restdocs/payload/PayloadDocumentation.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 9a552c92e..49456a11a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Map; +import org.springframework.restdocs.snippet.Attributes; + /** * Static factory methods for documenting a RESTful API's request and response payloads. * @@ -452,6 +454,7 @@ public static List applyPathPrefix(String pathPrefix, pathPrefix + descriptor.getPath()) .description(descriptor.getDescription()) .type(descriptor.getType()); + prefixedDescriptor.attributes(convertAttributesToArray(descriptor.getAttributes())); if (descriptor.isIgnored()) { prefixedDescriptor.ignored(); } @@ -463,4 +466,12 @@ public static List applyPathPrefix(String pathPrefix, return prefixedDescriptors; } + private static Attributes.Attribute[] convertAttributesToArray(Map attributes) { + List attributesAsList = new ArrayList<>(); + for (Map.Entry entry : attributes.entrySet()) { + attributesAsList.add(Attributes.key(entry.getKey()).value(entry.getValue())); + } + return attributesAsList.toArray(new Attributes.Attribute[] {}); + } + } From 7487f413631544087a50aedea0bb21a024c79b88 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Oct 2016 11:25:14 +0100 Subject: [PATCH 189/898] Polish "Copy attributes when applying path prefix to field descriptors" - Apply code formatting - Test copying of attributes (and other parts of a field descriptor) in new PayloadDocumentationTests Closes gh-314 --- .../payload/PayloadDocumentation.java | 16 ++-- .../payload/PayloadDocumentationTests.java | 93 +++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 49456a11a..c4a57c254 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -22,6 +22,7 @@ import java.util.Map; import org.springframework.restdocs.snippet.Attributes; +import org.springframework.restdocs.snippet.Attributes.Attribute; /** * Static factory methods for documenting a RESTful API's request and response payloads. @@ -453,8 +454,8 @@ public static List applyPathPrefix(String pathPrefix, FieldDescriptor prefixedDescriptor = new FieldDescriptor( pathPrefix + descriptor.getPath()) .description(descriptor.getDescription()) - .type(descriptor.getType()); - prefixedDescriptor.attributes(convertAttributesToArray(descriptor.getAttributes())); + .type(descriptor.getType()) + .attributes(asArray(descriptor.getAttributes())); if (descriptor.isIgnored()) { prefixedDescriptor.ignored(); } @@ -466,12 +467,13 @@ public static List applyPathPrefix(String pathPrefix, return prefixedDescriptors; } - private static Attributes.Attribute[] convertAttributesToArray(Map attributes) { - List attributesAsList = new ArrayList<>(); - for (Map.Entry entry : attributes.entrySet()) { - attributesAsList.add(Attributes.key(entry.getKey()).value(entry.getValue())); + private static Attribute[] asArray(Map attributeMap) { + List attributes = new ArrayList<>(); + for (Map.Entry attribute : attributeMap.entrySet()) { + attributes + .add(Attributes.key(attribute.getKey()).value(attribute.getValue())); } - return attributesAsList.toArray(new Attributes.Attribute[] {}); + return attributes.toArray(new Attribute[attributes.size()]); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java new file mode 100644 index 000000000..e17671aa2 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2014-2016 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 java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.payload.PayloadDocumentation.applyPathPrefix; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link PayloadDocumentation}. + * + * @author Andy Wilkinson + */ +public class PayloadDocumentationTests { + + @Test + public void applyPathPrefixAppliesPrefixToDescriptorPaths() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo"), fieldWithPath("charlie"))); + assertThat(descriptors.size(), is(equalTo(2))); + assertThat(descriptors.get(0).getPath(), is(equalTo("alpha.bravo"))); + } + + @Test + public void applyPathPrefixCopiesIgnored() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").ignored())); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).isIgnored(), is(true)); + } + + @Test + public void applyPathPrefixCopiesOptional() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").optional())); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).isOptional(), is(true)); + } + + @Test + public void applyPathPrefixCopiesDescription() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").description("Some field"))); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).getDescription(), + is(equalTo((Object) "Some field"))); + } + + @Test + public void applyPathPrefixCopiesType() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").type(JsonFieldType.OBJECT))); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).getType(), + is(equalTo((Object) JsonFieldType.OBJECT))); + } + + @Test + public void applyPathPrefixCopiesAttributes() { + List descriptors = applyPathPrefix("alpha.", + Arrays.asList(fieldWithPath("bravo").attributes(key("a").value("alpha"), + key("b").value("bravo")))); + assertThat(descriptors.size(), is(equalTo(1))); + assertThat(descriptors.get(0).getAttributes().size(), is(equalTo(2))); + assertThat(descriptors.get(0).getAttributes().get("a"), + is(equalTo((Object) "alpha"))); + assertThat(descriptors.get(0).getAttributes().get("b"), + is(equalTo((Object) "bravo"))); + } + +} From 4f19294220eea0ed442f9d3d0718f7135ff11981 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Oct 2016 12:27:03 +0100 Subject: [PATCH 190/898] Allow a TemplatedSnippet to produce multiple snippets from same template Previously a TemplatedSnippet could only write snippets with the same name as its template. This meant that the same template could not be used to produce multiple snippets with different names. This commit separates the snippet name from the template name, thereby allowing the snippet name to vary while the template name remains the same. For backwards compatibility, the default behaviour is for the template name and snippet name to remain the same. The new overloaded constructor can be used when the names need to differ. Closes gh-320 --- .../restdocs/snippet/TemplatedSnippet.java | 28 ++- .../restdocs/AbstractSnippetTests.java | 6 +- .../restdocs/cli/CurlRequestSnippetTests.java | 60 +++--- .../cli/HttpieRequestSnippetTests.java | 60 +++--- .../RequestHeadersSnippetFailureTests.java | 4 +- .../headers/RequestHeadersSnippetTests.java | 14 +- .../ResponseHeadersSnippetFailureTests.java | 4 +- .../headers/ResponseHeadersSnippetTests.java | 14 +- .../http/HttpRequestSnippetTests.java | 46 ++--- .../http/HttpResponseSnippetTests.java | 14 +- .../hypermedia/LinksSnippetFailureTests.java | 4 +- .../hypermedia/LinksSnippetTests.java | 20 +- .../AsciidoctorRequestFieldsSnippetTests.java | 6 +- .../RequestFieldsSnippetFailureTests.java | 5 +- .../payload/RequestFieldsSnippetTests.java | 30 +-- .../ResponseFieldsSnippetFailureTests.java | 4 +- .../payload/ResponseFieldsSnippetTests.java | 38 ++-- .../PathParametersSnippetFailureTests.java | 4 +- .../request/PathParametersSnippetTests.java | 22 +-- .../RequestParametersSnippetFailureTests.java | 4 +- .../RequestParametersSnippetTests.java | 22 +-- .../RequestPartsSnippetFailureTests.java | 4 +- .../request/RequestPartsSnippetTests.java | 20 +- .../snippet/TemplatedSnippetTests.java | 29 ++- .../restdocs/test/ExpectedSnippet.java | 162 ---------------- .../restdocs/test/ExpectedSnippets.java | 177 ++++++++++++++++++ .../templates/multiple-snippets.snippet | 0 27 files changed, 432 insertions(+), 369 deletions(-) delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java create mode 100644 spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/multiple-snippets.snippet diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java index dedfa98a3..cfb584fb6 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/snippet/TemplatedSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -38,15 +38,34 @@ public abstract class TemplatedSnippet implements Snippet { private final String snippetName; + private final String templateName; + /** * Creates a new {@code TemplatedSnippet} that will produce a snippet with the given - * {@code snippetName}. The given {@code attributes} will be included in the model - * during rendering of the template. + * {@code snippetName}. The {@code snippetName} will also be used as the name of the + * template. The given {@code attributes} will be included in the model during + * rendering of the template. * * @param snippetName The name of the snippet * @param attributes The additional attributes + * @see #TemplatedSnippet(String, String, Map) */ protected TemplatedSnippet(String snippetName, Map attributes) { + this(snippetName, snippetName, attributes); + } + + /** + * Creates a new {@code TemplatedSnippet} that will produce a snippet with the given + * {@code snippetName} using a template with the given {@code templateName}. The given + * {@code attributes} will be included in the model during rendering of the template. + * + * @param snippetName The name of the snippet + * @param templateName The name of the template + * @param attributes The additional attributes + */ + protected TemplatedSnippet(String snippetName, String templateName, + Map attributes) { + this.templateName = templateName; this.snippetName = snippetName; if (attributes != null) { this.attributes.putAll(attributes); @@ -65,7 +84,8 @@ public void document(Operation operation) throws IOException { model.putAll(this.attributes); TemplateEngine templateEngine = (TemplateEngine) operation.getAttributes() .get(TemplateEngine.class.getName()); - writer.append(templateEngine.compileTemplate(this.snippetName).render(model)); + writer.append( + templateEngine.compileTemplate(this.templateName).render(model)); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index 41b18ffda..dbca4c0e9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -27,7 +27,7 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpStatus; import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import org.springframework.restdocs.test.SnippetMatchers; import org.springframework.restdocs.test.SnippetMatchers.CodeBlockMatcher; @@ -50,7 +50,7 @@ public abstract class AbstractSnippetTests { protected final TemplateFormat templateFormat; @Rule - public ExpectedSnippet snippet; + public ExpectedSnippets snippets; @Rule public OperationBuilder operationBuilder; @@ -62,7 +62,7 @@ public static List parameters() { } public AbstractSnippetTests(String name, TemplateFormat templateFormat) { - this.snippet = new ExpectedSnippet(templateFormat); + this.snippets = new ExpectedSnippets(templateFormat); this.templateFormat = templateFormat; this.operationBuilder = new OperationBuilder(this.templateFormat); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index c9e2ead72..fc4cf64c7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -55,7 +55,7 @@ public CurlRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectCurlRequest().withContents( + this.snippets.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo").build()); @@ -63,7 +63,7 @@ public void getRequest() throws IOException { @Test public void getRequestWithParameter() throws IOException { - this.snippet.expectCurlRequest().withContents( + this.snippets.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo?a=alpha' -i")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").param("a", "alpha").build()); @@ -71,7 +71,7 @@ public void getRequestWithParameter() throws IOException { @Test public void nonGetRequest() throws IOException { - this.snippet.expectCurlRequest().withContents( + this.snippets.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").build()); @@ -79,7 +79,7 @@ public void nonGetRequest() throws IOException { @Test public void requestWithContent() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -d 'content'")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").content("content").build()); @@ -87,7 +87,7 @@ public void requestWithContent() throws IOException { @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param=value' -i")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").build()); @@ -96,7 +96,7 @@ public void getRequestWithQueryString() throws IOException { @Test public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param=value' -i")); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?param=value") @@ -106,7 +106,7 @@ public void getRequestWithTotallyOverlappingQueryStringAndParameters() @Test public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -115,7 +115,7 @@ public void getRequestWithPartiallyOverlappingQueryStringAndParameters() @Test public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); @@ -123,7 +123,7 @@ public void getRequestWithDisjointQueryStringAndParameters() throws IOException @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest().withContents( + this.snippets.expectCurlRequest().withContents( codeBlock("bash").content("$ curl 'http://localhost/foo?param' -i")); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?param").build()); @@ -131,7 +131,7 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param=value' -i -X POST")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").method("POST").build()); @@ -139,7 +139,7 @@ public void postRequestWithQueryString() throws IOException { @Test public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?param' -i -X POST")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param").method("POST").build()); @@ -147,7 +147,7 @@ public void postRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -156,7 +156,7 @@ public void postRequestWithOneParameter() throws IOException { @Test public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1='")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").param("k1").build()); @@ -164,7 +164,7 @@ public void postRequestWithOneParameterWithNoValue() throws IOException { @Test public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); @@ -175,7 +175,7 @@ public void postRequestWithMultipleParameters() throws IOException { @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -184,7 +184,7 @@ public void postRequestWithUrlEncodedParameter() throws IOException { @Test public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( + this.snippets.expectCurlRequest().withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -194,7 +194,7 @@ public void postRequestWithDisjointQueryStringAndParameter() throws IOException @Test public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i -X POST")); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") @@ -204,7 +204,7 @@ public void postRequestWithTotallyOverlappingQueryStringAndParameters() @Test public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash").content( + this.snippets.expectCurlRequest().withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -213,7 +213,7 @@ public void postRequestWithPartiallyOverlappingQueryStringAndParameters() @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'")); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); @@ -221,7 +221,7 @@ public void putRequestWithOneParameter() throws IOException { @Test public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X PUT" + " -d 'k1=v1&k1=v1-bis&k2=v2'")); @@ -232,7 +232,7 @@ public void putRequestWithMultipleParameters() throws IOException { @Test public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -241,7 +241,7 @@ public void putRequestWithUrlEncodedParameter() throws IOException { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + " -H 'Content-Type: application/json' -H 'a: alpha'")); new CurlRequestSnippet() @@ -256,7 +256,7 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'metadata={\"description\": \"foo\"}'"; - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document(this.operationBuilder .request("http://localhost/upload").method("POST") @@ -269,7 +269,7 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png;type=image/png'"; - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -285,7 +285,7 @@ public void multipartPost() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png'"; - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -301,7 +301,7 @@ public void multipartPostWithParameters() throws IOException { + "'Content-Type: multipart/form-data' -F " + "'image=@documents/images/example.png' -F 'a=apple' -F 'a=avocado' " + "-F 'b=banana'"; - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); new CurlRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -314,7 +314,7 @@ public void multipartPostWithParameters() throws IOException { @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { - this.snippet.expectCurlRequest().withContents(codeBlock("bash") + this.snippets.expectCurlRequest().withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo' -i -u 'user:secret'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -326,7 +326,7 @@ public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException @Test public void customAttributes() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(containsString("curl request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("curl-request")) @@ -344,7 +344,7 @@ public void customAttributes() throws IOException { @Test public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content( "$ curl 'http://localhost/foo' -i" + " -H 'Host: api.example.com'" + " -H 'Content-Type: application/json' -H 'a: alpha'")); @@ -358,7 +358,7 @@ public void customHostHeaderIsIncluded() throws IOException { @Test public void postWithContentAndParameters() throws IOException { - this.snippet.expectCurlRequest() + this.snippets.expectCurlRequest() .withContents(codeBlock("bash") .content("$ curl 'http://localhost/foo?a=alpha&b=bravo' -i " + "-X POST -d 'Some content'")); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 527a2c7f8..d5dc8a399 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -56,7 +56,7 @@ public HttpieRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo").build()); @@ -64,7 +64,7 @@ public void getRequest() throws IOException { @Test public void getRequestWithParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo?a=alpha'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").param("a", "alpha").build()); @@ -72,7 +72,7 @@ public void getRequestWithParameter() throws IOException { @Test public void nonGetRequest() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http POST 'http://localhost/foo'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").build()); @@ -80,7 +80,7 @@ public void nonGetRequest() throws IOException { @Test public void requestWithContent() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ echo 'content' | http GET 'http://localhost/foo'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").content("content").build()); @@ -88,7 +88,7 @@ public void requestWithContent() throws IOException { @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http GET 'http://localhost/foo?param=value'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").build()); @@ -97,7 +97,7 @@ public void getRequestWithQueryString() throws IOException { @Test public void getRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http GET 'http://localhost/foo?param=value'")); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?param=value") @@ -107,7 +107,7 @@ public void getRequestWithTotallyOverlappingQueryStringAndParameters() @Test public void getRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -116,7 +116,7 @@ public void getRequestWithPartiallyOverlappingQueryStringAndParameters() @Test public void getRequestWithDisjointQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http GET 'http://localhost/foo?a=alpha&b=bravo'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?a=alpha").param("b", "bravo").build()); @@ -124,7 +124,7 @@ public void getRequestWithDisjointQueryStringAndParameters() throws IOException @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo?param'")); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?param").build()); @@ -132,7 +132,7 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithQueryString() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http POST 'http://localhost/foo?param=value'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param=value").method("POST").build()); @@ -140,7 +140,7 @@ public void postRequestWithQueryString() throws IOException { @Test public void postRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http POST 'http://localhost/foo?param'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?param").method("POST").build()); @@ -148,7 +148,7 @@ public void postRequestWithQueryStringWithNoValue() throws IOException { @Test public void postRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo' 'k1=v1'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -157,7 +157,7 @@ public void postRequestWithOneParameter() throws IOException { @Test public void postRequestWithOneParameterWithNoValue() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo' 'k1='")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").param("k1").build()); @@ -165,7 +165,7 @@ public void postRequestWithOneParameterWithNoValue() throws IOException { @Test public void postRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); @@ -176,7 +176,7 @@ public void postRequestWithMultipleParameters() throws IOException { @Test public void postRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -185,7 +185,7 @@ public void postRequestWithUrlEncodedParameter() throws IOException { @Test public void postRequestWithDisjointQueryStringAndParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -195,7 +195,7 @@ public void postRequestWithDisjointQueryStringAndParameter() throws IOException @Test public void postRequestWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http POST 'http://localhost/foo?a=alpha&b=bravo'")); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/foo?a=alpha&b=bravo") @@ -205,7 +205,7 @@ public void postRequestWithTotallyOverlappingQueryStringAndParameters() @Test public void postRequestWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form POST 'http://localhost/foo?a=alpha' 'b=bravo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?a=alpha") @@ -214,7 +214,7 @@ public void postRequestWithPartiallyOverlappingQueryStringAndParameters() @Test public void putRequestWithOneParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form PUT 'http://localhost/foo' 'k1=v1'")); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("PUT").param("k1", "v1").build()); @@ -222,7 +222,7 @@ public void putRequestWithOneParameter() throws IOException { @Test public void putRequestWithMultipleParameters() throws IOException { - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash") .content("$ http --form PUT 'http://localhost/foo'" + " 'k1=v1' 'k1=v1-bis' 'k2=v2'")); @@ -233,7 +233,7 @@ public void putRequestWithMultipleParameters() throws IOException { @Test public void putRequestWithUrlEncodedParameter() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --form PUT 'http://localhost/foo' 'k1=a&b'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -242,7 +242,7 @@ public void putRequestWithUrlEncodedParameter() throws IOException { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ http GET 'http://localhost/foo'" + " 'Content-Type:application/json' 'a:alpha'")); new HttpieRequestSnippet() @@ -257,7 +257,7 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'metadata'@<(echo '{\"description\": \"foo\"}')"); - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet().document(this.operationBuilder .request("http://localhost/upload").method("POST") @@ -271,7 +271,7 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -287,7 +287,7 @@ public void multipartPost() throws IOException { String expectedContent = String .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png'"); - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -303,7 +303,7 @@ public void multipartPostWithParameters() throws IOException { .format("$ http --form POST 'http://localhost/upload' \\%n" + " 'image'@'documents/images/example.png' 'a=apple' 'a=avocado'" + " 'b=banana'"); - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); new HttpieRequestSnippet().document( this.operationBuilder.request("http://localhost/upload").method("POST") @@ -316,7 +316,7 @@ public void multipartPostWithParameters() throws IOException { @Test public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { - this.snippet.expectHttpieRequest().withContents(codeBlock("bash") + this.snippets.expectHttpieRequest().withContents(codeBlock("bash") .content("$ http --auth 'user:secret' GET 'http://localhost/foo'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -328,7 +328,7 @@ public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException @Test public void customAttributes() throws IOException { - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(containsString("httpie request title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("httpie-request")) @@ -346,7 +346,7 @@ public void customAttributes() throws IOException { @Test public void customHostHeaderIsIncluded() throws IOException { - this.snippet.expectHttpieRequest() + this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content( "$ http GET 'http://localhost/foo' 'Host:api.example.com'" + " 'Content-Type:application/json' 'a:alpha'")); @@ -360,7 +360,7 @@ public void customHostHeaderIsIncluded() throws IOException { @Test public void postWithContentAndParameters() throws IOException { - this.snippet.expectHttpieRequest().withContents( + this.snippets.expectHttpieRequest().withContents( codeBlock("bash").content("$ echo 'Some content' | http POST " + "'http://localhost/foo?a=alpha&b=bravo'")); new HttpieRequestSnippet().document(this.operationBuilder diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java index f1192567c..e2eba481a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; @@ -44,7 +44,7 @@ public class RequestHeadersSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index 373e83b70..ef87c2f72 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -49,7 +49,7 @@ public RequestHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestWithHeaders() throws IOException { - this.snippet.expectRequestHeaders() + this.snippets.expectRequestHeaders() .withContents(tableWithHeader("Name", "Description") .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") @@ -83,7 +83,7 @@ public void requestWithHeaders() throws IOException { @Test public void caseInsensitiveRequestHeaders() throws IOException { - this.snippet.expectRequestHeaders().withContents( + this.snippets.expectRequestHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) @@ -93,7 +93,7 @@ public void caseInsensitiveRequestHeaders() throws IOException { @Test public void undocumentedRequestHeader() throws IOException { - this.snippet.expectRequestHeaders().withContents( + this.snippets.expectRequestHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) @@ -104,7 +104,7 @@ public void undocumentedRequestHeader() throws IOException { @Test public void requestHeadersWithCustomAttributes() throws IOException { - this.snippet.expectRequestHeaders().withContents(containsString("Custom title")); + this.snippets.expectRequestHeaders().withContents(containsString("Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-headers")) .willReturn(snippetResource("request-headers-with-title")); @@ -123,7 +123,7 @@ public void requestHeadersWithCustomAttributes() throws IOException { @Test public void requestHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectRequestHeaders().withContents(// + this.snippets.expectRequestHeaders().withContents(// tableWithHeader("Name", "Description", "Foo") .row("X-Test", "one", "alpha") .row("Accept-Encoding", "two", "bravo") @@ -152,7 +152,7 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestHeaders() + this.snippets.expectRequestHeaders() .withContents(tableWithHeader("Name", "Description") .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") @@ -178,7 +178,7 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectRequestHeaders().withContents( + this.snippets.expectRequestHeaders().withContents( tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new RequestHeadersSnippet( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java index e42d38179..72d38fccb 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetFailureTests.java @@ -24,7 +24,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; @@ -44,7 +44,7 @@ public class ResponseHeadersSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java index ba572598d..7f5fe1d64 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/ResponseHeadersSnippetTests.java @@ -49,7 +49,7 @@ public ResponseHeadersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one") .row("`Content-Type`", "two").row("`Etag`", "three") .row("`Cache-Control`", "five").row("`Vary`", "six")); @@ -71,7 +71,7 @@ public void responseWithHeaders() throws IOException { @Test public void caseInsensitiveResponseHeaders() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))) @@ -81,7 +81,7 @@ public void caseInsensitiveResponseHeaders() throws IOException { @Test public void undocumentedResponseHeader() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one")); new ResponseHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"))).document( @@ -91,7 +91,7 @@ public void undocumentedResponseHeader() throws IOException { @Test public void responseHeadersWithCustomAttributes() throws IOException { - this.snippet.expectResponseHeaders().withContents(containsString("Custom title")); + this.snippets.expectResponseHeaders().withContents(containsString("Custom title")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-headers")) .willReturn(snippetResource("response-headers-with-title")); @@ -110,7 +110,7 @@ public void responseHeadersWithCustomAttributes() throws IOException { @Test public void responseHeadersWithCustomDescriptorAttributes() throws IOException { - this.snippet.expectResponseHeaders() + this.snippets.expectResponseHeaders() .withContents(tableWithHeader("Name", "Description", "Foo") .row("X-Test", "one", "alpha").row("Content-Type", "two", "bravo") .row("Etag", "three", "charlie")); @@ -138,7 +138,7 @@ public void responseHeadersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row("`X-Test`", "one") .row("`Content-Type`", "two").row("`Etag`", "three") .row("`Cache-Control`", "five").row("`Vary`", "six")); @@ -157,7 +157,7 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectResponseHeaders().withContents( + this.snippets.expectResponseHeaders().withContents( tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new ResponseHeadersSnippet( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 6ba4d65a3..88c27ee1e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -51,7 +51,7 @@ public HttpRequestSnippetTests(String name, TemplateFormat templateFormat) { @Test public void getRequest() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") .header(HttpHeaders.HOST, "localhost")); @@ -61,7 +61,7 @@ public void getRequest() throws IOException { @Test public void getRequestWithParameters() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?b=bravo") .header("Alpha", "a").header(HttpHeaders.HOST, "localhost")); @@ -72,7 +72,7 @@ public void getRequestWithParameters() throws IOException { @Test public void getRequestWithPort() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo").header("Alpha", "a") .header(HttpHeaders.HOST, "localhost:8080")); @@ -82,7 +82,7 @@ public void getRequestWithPort() throws IOException { @Test public void getRequestWithQueryString() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?bar=baz") .header(HttpHeaders.HOST, "localhost")); @@ -92,7 +92,7 @@ public void getRequestWithQueryString() throws IOException { @Test public void getRequestWithQueryStringWithNoValue() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?bar") .header(HttpHeaders.HOST, "localhost")); @@ -102,7 +102,7 @@ public void getRequestWithQueryStringWithNoValue() throws IOException { @Test public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") .header(HttpHeaders.HOST, "localhost")); @@ -113,7 +113,7 @@ public void getWithPartiallyOverlappingQueryStringAndParameters() throws IOExcep @Test public void getWithTotallyOverlappingQueryStringAndParameters() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo?a=alpha&b=bravo") .header(HttpHeaders.HOST, "localhost")); @@ -125,7 +125,7 @@ public void getWithTotallyOverlappingQueryStringAndParameters() throws IOExcepti @Test public void postRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -137,7 +137,7 @@ public void postRequestWithContent() throws IOException { @Test public void postRequestWithContentAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -151,7 +151,7 @@ public void postRequestWithContentAndParameters() throws IOException { public void postRequestWithContentAndDisjointQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -165,7 +165,7 @@ public void postRequestWithContentAndDisjointQueryStringAndParameters() public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -179,7 +179,7 @@ public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameter public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -193,7 +193,7 @@ public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters( public void postRequestWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header("Content-Type", "text/plain;charset=UTF-8") .header(HttpHeaders.HOST, "localhost") @@ -208,7 +208,7 @@ public void postRequestWithCharset() throws IOException { @Test public void postRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") @@ -221,7 +221,7 @@ public void postRequestWithParameter() throws IOException { @Test public void postRequestWithParameterWithNoValue() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") @@ -234,7 +234,7 @@ public void postRequestWithParameterWithNoValue() throws IOException { @Test public void putRequestWithContent() throws IOException { String content = "Hello, world"; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost").content(content).header( HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); @@ -245,7 +245,7 @@ public void putRequestWithContent() throws IOException { @Test public void putRequestWithParameter() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") .header(HttpHeaders.HOST, "localhost") .header("Content-Type", "application/x-www-form-urlencoded") @@ -260,7 +260,7 @@ public void putRequestWithParameter() throws IOException { public void multipartPost() throws IOException { String expectedContent = createPart(String.format( "Content-Disposition: " + "form-data; " + "name=image%n%n<< data >>")); - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -286,7 +286,7 @@ public void multipartPostWithParameters() throws IOException { String filePart = createPart(String .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = param1Part + param2Part + param3Part + filePart; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -306,7 +306,7 @@ public void multipartPostWithParameterWithNoValue() throws IOException { String filePart = createPart(String .format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); String expectedContent = paramPart + filePart; - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -323,7 +323,7 @@ public void multipartPostWithContentType() throws IOException { String expectedContent = createPart( String.format("Content-Disposition: form-data; name=image%nContent-Type: " + "image/png%n%n<< data >>")); - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/upload") .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) @@ -339,7 +339,7 @@ public void multipartPostWithContentType() throws IOException { @Test public void getRequestWithCustomHost() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.GET, "/foo") .header(HttpHeaders.HOST, "api.example.com")); new HttpRequestSnippet() @@ -349,7 +349,7 @@ public void getRequestWithCustomHost() throws IOException { @Test public void requestWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpRequest() + this.snippets.expectHttpRequest() .withContents(containsString("Title for the request")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-request")) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java index 8560fd399..bbfa255ab 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpResponseSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -49,13 +49,13 @@ public HttpResponseSnippetTests(String name, TemplateFormat templateFormat) { @Test public void basicResponse() throws IOException { - this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK)); + this.snippets.expectHttpResponse().withContents(httpResponse(HttpStatus.OK)); new HttpResponseSnippet().document(this.operationBuilder.build()); } @Test public void nonOkResponse() throws IOException { - this.snippet.expectHttpResponse() + this.snippets.expectHttpResponse() .withContents(httpResponse(HttpStatus.BAD_REQUEST)); new HttpResponseSnippet().document(this.operationBuilder.response() .status(HttpStatus.BAD_REQUEST.value()).build()); @@ -63,7 +63,7 @@ public void nonOkResponse() throws IOException { @Test public void responseWithHeaders() throws IOException { - this.snippet.expectHttpResponse().withContents(httpResponse(HttpStatus.OK) + this.snippets.expectHttpResponse().withContents(httpResponse(HttpStatus.OK) .header("Content-Type", "application/json").header("a", "alpha")); new HttpResponseSnippet() .document(this.operationBuilder.response() @@ -75,7 +75,7 @@ public void responseWithHeaders() throws IOException { @Test public void responseWithContent() throws IOException { String content = "content"; - this.snippet.expectHttpResponse() + this.snippets.expectHttpResponse() .withContents(httpResponse(HttpStatus.OK).content(content) .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpResponseSnippet() @@ -86,7 +86,7 @@ public void responseWithContent() throws IOException { public void responseWithCharset() throws IOException { String japaneseContent = "\u30b3\u30f3\u30c6\u30f3\u30c4"; byte[] contentBytes = japaneseContent.getBytes("UTF-8"); - this.snippet.expectHttpResponse() + this.snippets.expectHttpResponse() .withContents(httpResponse(HttpStatus.OK) .header("Content-Type", "text/plain;charset=UTF-8") .content(japaneseContent) @@ -98,7 +98,7 @@ public void responseWithCharset() throws IOException { @Test public void responseWithCustomSnippetAttributes() throws IOException { - this.snippet.expectHttpResponse() + this.snippets.expectHttpResponse() .withContents(containsString("Title for the response")); TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("http-response")) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java index f8e0cf762..f8d9c6a04 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetFailureTests.java @@ -25,7 +25,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -43,7 +43,7 @@ public class LinksSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java index 3236bd8ec..7f6f98d32 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/hypermedia/LinksSnippetTests.java @@ -47,7 +47,7 @@ public LinksSnippetTests(String name, TemplateFormat templateFormat) { @Test public void ignoredLink() throws IOException { - this.snippet.expectLinks().withContents( + this.snippets.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -59,7 +59,7 @@ public void ignoredLink() throws IOException { @Test public void allUndocumentedLinksCanBeIgnored() throws IOException { - this.snippet.expectLinks().withContents( + this.snippets.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -70,7 +70,7 @@ public void allUndocumentedLinksCanBeIgnored() throws IOException { @Test public void presentOptionalLink() throws IOException { - this.snippet.expectLinks().withContents( + this.snippets.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("foo", "blah")), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) @@ -79,7 +79,7 @@ public void presentOptionalLink() throws IOException { @Test public void missingOptionalLink() throws IOException { - this.snippet.expectLinks().withContents( + this.snippets.expectLinks().withContents( tableWithHeader("Relation", "Description").row("`foo`", "bar")); new LinksSnippet(new StubLinkExtractor(), Arrays.asList(new LinkDescriptor("foo").description("bar").optional())) @@ -88,7 +88,7 @@ public void missingOptionalLink() throws IOException { @Test public void documentedLinks() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + this.snippets.expectLinks().withContents(tableWithHeader("Relation", "Description") .row("`a`", "one").row("`b`", "two")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -100,7 +100,7 @@ public void documentedLinks() throws IOException { @Test public void linkDescriptionFromTitleInPayload() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + this.snippets.expectLinks().withContents(tableWithHeader("Relation", "Description") .row("`a`", "one").row("`b`", "Link b")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha", "Link a"), @@ -114,7 +114,7 @@ public void linksWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) .willReturn(snippetResource("links-with-title")); - this.snippet.expectLinks().withContents(containsString("Title for the links")); + this.snippets.expectLinks().withContents(containsString("Title for the links")); new LinksSnippet( new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -134,7 +134,7 @@ public void linksWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("links")) .willReturn(snippetResource("links-with-extra-column")); - this.snippet.expectLinks() + this.snippets.expectLinks() .withContents(tableWithHeader("Relation", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -154,7 +154,7 @@ public void linksWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + this.snippets.expectLinks().withContents(tableWithHeader("Relation", "Description") .row("`a`", "one").row("`b`", "two")); HypermediaDocumentation .links(new StubLinkExtractor().withLinks(new Link("a", "alpha"), @@ -166,7 +166,7 @@ public void additionalDescriptors() throws IOException { @Test public void tableCellContentIsEscapedWhenNecessary() throws IOException { - this.snippet.expectLinks().withContents(tableWithHeader("Relation", "Description") + this.snippets.expectLinks().withContents(tableWithHeader("Relation", "Description") .row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")), Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two"))) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java index d1a83a662..9f737e3a7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/AsciidoctorRequestFieldsSnippetTests.java @@ -26,7 +26,7 @@ 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; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.mockito.BDDMockito.given; @@ -46,14 +46,14 @@ public class AsciidoctorRequestFieldsSnippetTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Test public void requestFieldsWithListDescription() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-list-description")); - this.snippet.expectRequestFields().withContents( + this.snippets.expectRequestFields().withContents( tableWithHeader(asciidoctor(), "Path", "Type", "Description") // .row("a", "String", String.format(" - one%n - two")) diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index 30d16443a..d6fd39986 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -28,7 +28,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; @@ -45,7 +45,8 @@ public class RequestFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets( + TemplateFormats.asciidoctor()); @Rule public OperationBuilder operationBuilder = new OperationBuilder( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 8970a43da..8e6d8414d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -50,7 +50,7 @@ public RequestFieldsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") .row("`a`", "`Object`", "three")); @@ -65,7 +65,7 @@ public void mapRequestWithFields() throws IOException { @Test public void arrayRequestWithFields() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`[]`", "`Array`", "one").row("`[]a.b`", "`Number`", "two") .row("`[]a.c`", "`String`", "three") @@ -83,7 +83,7 @@ public void arrayRequestWithFields() throws IOException { @Test public void ignoredRequestField() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); @@ -95,7 +95,7 @@ public void ignoredRequestField() throws IOException { @Test public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("b").description("Field b")), @@ -106,7 +106,7 @@ public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { @Test public void missingOptionalRequestField() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") @@ -118,7 +118,7 @@ public void missingOptionalRequestField() throws IOException { @Test public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description")); new RequestFieldsSnippet(Arrays .asList(fieldWithPath("a.b").description("one").ignored().optional())) @@ -128,7 +128,7 @@ public void missingIgnoredOptionalRequestFieldDoesNotRequireAType() @Test public void presentOptionalRequestField() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") @@ -142,7 +142,7 @@ public void requestFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-title")); - this.snippet.expectRequestFields().withContents(containsString("Custom title")); + this.snippets.expectRequestFields().withContents(containsString("Custom title")); new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), attributes( @@ -161,7 +161,7 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-fields")) .willReturn(snippetResource("request-fields-with-extra-column")); - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description", "Foo") .row("a.b", "Number", "one", "alpha") .row("a.c", "String", "two", "bravo") @@ -187,7 +187,7 @@ public void requestFieldsWithCustomDescriptorAttributes() throws IOException { @Test public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); @@ -199,7 +199,7 @@ public void fieldWithExplictExactlyMatchingType() throws IOException { @Test public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); @@ -211,7 +211,7 @@ public void fieldWithExplictVariesType() throws IOException { @Test public void xmlRequestFields() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", "`a`", "three")); @@ -230,7 +230,7 @@ public void xmlRequestFields() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") .row("`a`", "`Object`", "three")); @@ -245,7 +245,7 @@ public void additionalDescriptors() throws IOException { @Test public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") .row("`a.c`", "`String`", "three")); @@ -259,7 +259,7 @@ public void prefixedAdditionalDescriptors() throws IOException { @Test public void requestWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectRequestFields() + this.snippets.expectRequestFields() .withContents(tableWithHeader("Path", "Type", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java index 05b1b9c17..92c8da49a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetFailureTests.java @@ -27,7 +27,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.endsWith; @@ -48,7 +48,7 @@ public class ResponseFieldsSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index d22fdb332..59002896a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -50,7 +50,7 @@ public ResponseFieldsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void mapResponseWithFields() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") @@ -72,7 +72,7 @@ public void mapResponseWithFields() throws IOException { @Test public void arrayResponseWithFields() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`[]a.b`", "`Number`", "one") .row("`[]a.c`", "`String`", "two") @@ -88,7 +88,7 @@ public void arrayResponseWithFields() throws IOException { @Test public void arrayResponse() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`[]`", "`Array`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("[]").description("one"))) @@ -98,7 +98,7 @@ public void arrayResponse() throws IOException { @Test public void ignoredResponseField() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); @@ -110,7 +110,7 @@ public void ignoredResponseField() throws IOException { @Test public void allUndocumentedFieldsCanBeIgnored() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", "`Number`", "Field b")); @@ -125,7 +125,7 @@ public void responseFieldsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-fields")) .willReturn(snippetResource("response-fields-with-title")); - this.snippet.expectResponseFields().withContents(containsString("Custom title")); + this.snippets.expectResponseFields().withContents(containsString("Custom title")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")), attributes( @@ -141,7 +141,7 @@ public void responseFieldsWithCustomAttributes() throws IOException { @Test public void missingOptionalResponseField() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") @@ -152,7 +152,7 @@ public void missingOptionalResponseField() throws IOException { @Test public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description")); new ResponseFieldsSnippet(Arrays .asList(fieldWithPath("a.b").description("one").ignored().optional())) @@ -161,7 +161,7 @@ public void missingIgnoredOptionalResponseFieldDoesNotRequireAType() @Test public void presentOptionalResponseField() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a.b`", "`String`", "one")); new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a.b").description("one") @@ -175,7 +175,7 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("response-fields")) .willReturn(snippetResource("response-fields-with-extra-column")); - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description", "Foo") .row("a.b", "Number", "one", "alpha") .row("a.c", "String", "two", "bravo") @@ -201,7 +201,7 @@ public void responseFieldsWithCustomDescriptorAttributes() throws IOException { @Test public void fieldWithExplictExactlyMatchingType() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Number`", "one")); @@ -213,7 +213,7 @@ public void fieldWithExplictExactlyMatchingType() throws IOException { @Test public void fieldWithExplictVariesType() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", "`Varies`", "one")); @@ -225,7 +225,7 @@ public void fieldWithExplictVariesType() throws IOException { @Test public void xmlResponseFields() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a/b`", "`b`", "one").row("`a/c`", "`c`", "two").row("`a`", "`a`", "three")); @@ -243,7 +243,7 @@ public void xmlResponseFields() throws IOException { @Test public void xmlAttribute() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( @@ -259,7 +259,7 @@ public void xmlAttribute() throws IOException { @Test public void missingOptionalXmlAttribute() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`b`", "one").row("`a/@id`", "`c`", "two")); new ResponseFieldsSnippet( @@ -275,7 +275,7 @@ public void missingOptionalXmlAttribute() throws IOException { @Test public void undocumentedAttributeDoesNotCauseFailure() throws IOException { - this.snippet.expectResponseFields().withContents( + this.snippets.expectResponseFields().withContents( tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); new ResponseFieldsSnippet( Arrays.asList(fieldWithPath("a").description("one").type("a"))).document( @@ -287,7 +287,7 @@ public void undocumentedAttributeDoesNotCauseFailure() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`id`", "`Number`", "one").row("`date`", "`String`", "two") .row("`assets`", "`Array`", "three") @@ -309,7 +309,7 @@ public void additionalDescriptors() throws IOException { @Test public void prefixedAdditionalDescriptors() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description") .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") .row("`a.c`", "`String`", "three")); @@ -323,7 +323,7 @@ public void prefixedAdditionalDescriptors() throws IOException { @Test public void responseWithFieldsWithEscapedContent() throws IOException { - this.snippet.expectResponseFields() + this.snippets.expectResponseFields() .withContents(tableWithHeader("Path", "Type", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"), escapeIfNecessary("three|four"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java index 26f0f6b09..27a897d5d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetFailureTests.java @@ -26,7 +26,7 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -45,7 +45,7 @@ public class PathParametersSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java index c74f5d401..b16a8a95f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/PathParametersSnippetTests.java @@ -49,7 +49,7 @@ public PathParametersSnippetTests(String name, TemplateFormat templateFormat) { @Test public void pathParameters() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), @@ -63,7 +63,7 @@ public void pathParameters() throws IOException { @Test public void ignoredPathParameter() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), @@ -77,7 +77,7 @@ public void ignoredPathParameter() throws IOException { @Test public void allUndocumentedPathParametersCanBeIgnored() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description").row("`b`", "two")); new PathParametersSnippet( @@ -89,7 +89,7 @@ public void allUndocumentedPathParametersCanBeIgnored() throws IOException { @Test public void missingOptionalPathParameter() throws IOException { - this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( + this.snippets.expectPathParameters().withContents(tableWithTitleAndHeader( this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", "Parameter", "Description").row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), @@ -101,7 +101,7 @@ public void missingOptionalPathParameter() throws IOException { @Test public void presentOptionalPathParameter() throws IOException { - this.snippet.expectPathParameters().withContents(tableWithTitleAndHeader( + this.snippets.expectPathParameters().withContents(tableWithTitleAndHeader( this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}" : "`/{a}`", "Parameter", "Description").row("`a`", "one")); new PathParametersSnippet( @@ -113,7 +113,7 @@ public void presentOptionalPathParameter() throws IOException { @Test public void pathParametersWithQueryString() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), @@ -127,7 +127,7 @@ public void pathParametersWithQueryString() throws IOException { @Test public void pathParametersWithQueryStringWithParameters() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new PathParametersSnippet(Arrays.asList(parameterWithName("a").description("one"), @@ -144,7 +144,7 @@ public void pathParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")) .willReturn(snippetResource("path-parameters-with-title")); - this.snippet.expectPathParameters().withContents(containsString("The title")); + this.snippets.expectPathParameters().withContents(containsString("The title")); new PathParametersSnippet( Arrays.asList( @@ -168,7 +168,7 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("path-parameters")) .willReturn(snippetResource("path-parameters-with-extra-column")); - this.snippet.expectPathParameters() + this.snippets.expectPathParameters() .withContents(tableWithHeader("Parameter", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -188,7 +188,7 @@ public void pathParametersWithCustomDescriptorAttributes() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectPathParameters().withContents( + this.snippets.expectPathParameters().withContents( tableWithTitleAndHeader(getTitle(), "Parameter", "Description") .row("`a`", "one").row("`b`", "two")); RequestDocumentation.pathParameters(parameterWithName("a").description("one")) @@ -201,7 +201,7 @@ public void additionalDescriptors() throws IOException { @Test public void pathParametersWithEscapedContent() throws IOException { - this.snippet.expectPathParameters() + this.snippets.expectPathParameters() .withContents(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java index ce50cc892..a979f65ef 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetFailureTests.java @@ -25,7 +25,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -44,7 +44,7 @@ public class RequestParametersSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java index cffbb671a..ce2cc4d4a 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestParametersSnippetTests.java @@ -48,7 +48,7 @@ public RequestParametersSnippetTests(String name, TemplateFormat templateFormat) @Test public void requestParameters() throws IOException { - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( @@ -60,7 +60,7 @@ public void requestParameters() throws IOException { @Test public void requestParameterWithNoValue() throws IOException { - this.snippet.expectRequestParameters().withContents( + this.snippets.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one"))) @@ -70,7 +70,7 @@ public void requestParameterWithNoValue() throws IOException { @Test public void ignoredRequestParameter() throws IOException { - this.snippet.expectRequestParameters().withContents( + this.snippets.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet(Arrays.asList(parameterWithName("a").ignored(), parameterWithName("b").description("two"))) @@ -80,7 +80,7 @@ public void ignoredRequestParameter() throws IOException { @Test public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { - this.snippet.expectRequestParameters().withContents( + this.snippets.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`b`", "two")); new RequestParametersSnippet( Arrays.asList(parameterWithName("b").description("two")), true) @@ -90,7 +90,7 @@ public void allUndocumentedRequestParametersCanBeIgnored() throws IOException { @Test public void missingOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); new RequestParametersSnippet( @@ -102,7 +102,7 @@ public void missingOptionalRequestParameter() throws IOException { @Test public void presentOptionalRequestParameter() throws IOException { - this.snippet.expectRequestParameters().withContents( + this.snippets.expectRequestParameters().withContents( tableWithHeader("Parameter", "Description").row("`a`", "one")); new RequestParametersSnippet( Arrays.asList(parameterWithName("a").description("one").optional())) @@ -115,7 +115,7 @@ public void requestParametersWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-title")); - this.snippet.expectRequestParameters().withContents(containsString("The title")); + this.snippets.expectRequestParameters().withContents(containsString("The title")); new RequestParametersSnippet( Arrays.asList( @@ -140,7 +140,7 @@ public void requestParametersWithCustomDescriptorAttributes() throws IOException TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-extra-column")); - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -164,7 +164,7 @@ public void requestParametersWithOptionalColumn() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parameters")) .willReturn(snippetResource("request-parameters-with-optional-column")); - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Optional", "Description") .row("a", "true", "one").row("b", "false", "two")); @@ -183,7 +183,7 @@ public void requestParametersWithOptionalColumn() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description") .row("`a`", "one").row("`b`", "two")); RequestDocumentation.requestParameters(parameterWithName("a").description("one")) @@ -194,7 +194,7 @@ public void additionalDescriptors() throws IOException { @Test public void requestParametersWithEscapedContent() throws IOException { - this.snippet.expectRequestParameters() + this.snippets.expectRequestParameters() .withContents(tableWithHeader("Parameter", "Description").row( escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java index 522598201..935e2d02c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetFailureTests.java @@ -25,7 +25,7 @@ import org.junit.rules.ExpectedException; import org.springframework.restdocs.snippet.SnippetException; -import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.ExpectedSnippets; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -44,7 +44,7 @@ public class RequestPartsSnippetFailureTests { public OperationBuilder operationBuilder = new OperationBuilder(asciidoctor()); @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(asciidoctor()); + public ExpectedSnippets snippets = new ExpectedSnippets(asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java index ecdce573a..f49d299a5 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/request/RequestPartsSnippetTests.java @@ -48,7 +48,7 @@ public RequestPartsSnippetTests(String name, TemplateFormat templateFormat) { @Test public void requestParts() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"), @@ -60,7 +60,7 @@ public void requestParts() throws IOException { @Test public void ignoredRequestPart() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(), partWithName("b").description("two"))) @@ -71,7 +71,7 @@ public void ignoredRequestPart() throws IOException { @Test public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`b`", "two")); new RequestPartsSnippet(Arrays.asList(partWithName("b").description("two")), true) .document(this.operationBuilder.request("http://localhost") @@ -81,7 +81,7 @@ public void allUndocumentedRequestPartsCanBeIgnored() throws IOException { @Test public void missingOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); new RequestPartsSnippet( @@ -93,7 +93,7 @@ public void missingOptionalRequestPart() throws IOException { @Test public void presentOptionalRequestPart() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one")); new RequestPartsSnippet( Arrays.asList(partWithName("a").description("one").optional())) @@ -106,7 +106,7 @@ public void requestPartsWithCustomAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-title")); - this.snippet.expectRequestParts().withContents(containsString("The title")); + this.snippets.expectRequestParts().withContents(containsString("The title")); new RequestPartsSnippet( Arrays.asList( @@ -131,7 +131,7 @@ public void requestPartsWithCustomDescriptorAttributes() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-extra-column")); - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description", "Foo") .row("a", "one", "alpha").row("b", "two", "bravo")); @@ -155,7 +155,7 @@ public void requestPartsWithOptionalColumn() throws IOException { TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); given(resolver.resolveTemplateResource("request-parts")) .willReturn(snippetResource("request-parts-with-optional-column")); - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Optional", "Description") .row("a", "true", "one").row("b", "false", "two")); @@ -174,7 +174,7 @@ public void requestPartsWithOptionalColumn() throws IOException { @Test public void additionalDescriptors() throws IOException { - this.snippet.expectRequestParts() + this.snippets.expectRequestParts() .withContents(tableWithHeader("Part", "Description").row("`a`", "one") .row("`b`", "two")); RequestDocumentation.requestParts(partWithName("a").description("one")) @@ -186,7 +186,7 @@ public void additionalDescriptors() throws IOException { @Test public void requestPartsWithEscapedContent() throws IOException { - this.snippet.expectRequestParts().withContents( + this.snippets.expectRequestParts().withContents( tableWithHeader("Part", "Description").row(escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two"))); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java index f64034c67..aea28d3ce 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/snippet/TemplatedSnippetTests.java @@ -16,13 +16,18 @@ package org.springframework.restdocs.snippet; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.junit.Rule; import org.junit.Test; import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.test.ExpectedSnippets; +import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -38,6 +43,13 @@ */ public class TemplatedSnippetTests { + @Rule + public OperationBuilder operationBuilder = new OperationBuilder( + TemplateFormats.asciidoctor()); + + @Rule + public ExpectedSnippets snippets = new ExpectedSnippets(TemplateFormats.asciidoctor()); + @Test public void attributesAreCopied() { Map attributes = new HashMap<>(); @@ -60,15 +72,30 @@ public void snippetName() { .getSnippetName(), is(equalTo("test"))); } + @Test + public void multipleSnippetsCanBeProducedFromTheSameTemplate() throws IOException { + this.snippets.expect("multiple-snippets-one"); + this.snippets.expect("multiple-snippets-two"); + new TestTemplatedSnippet("one", "multiple-snippets") + .document(this.operationBuilder.build()); + new TestTemplatedSnippet("two", "multiple-snippets") + .document(this.operationBuilder.build()); + } + private static class TestTemplatedSnippet extends TemplatedSnippet { + protected TestTemplatedSnippet(String snippetName, String templateName) { + super(templateName + "-" + snippetName, templateName, + Collections.emptyMap()); + } + protected TestTemplatedSnippet(Map attributes) { super("test", attributes); } @Override protected Map createModel(Operation operation) { - return null; + return new HashMap<>(); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java deleted file mode 100644 index 8036e98e0..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippet.java +++ /dev/null @@ -1,162 +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.test; - -import java.io.File; -import java.io.IOException; - -import org.hamcrest.Matcher; -import org.junit.runners.model.Statement; - -import org.springframework.restdocs.snippet.TemplatedSnippet; -import org.springframework.restdocs.templates.TemplateFormat; -import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; - -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; - -/** - * The {@code ExpectedSnippet} rule is used to verify that a {@link TemplatedSnippet} has - * generated the expected snippet. - * - * @author Andy Wilkinson - * @author Andreas Evers - */ -public class ExpectedSnippet extends OperationTestRule { - - private final TemplateFormat templateFormat; - - private final SnippetMatcher snippet; - - private String expectedName; - - private String expectedType; - - private File outputDirectory; - - public ExpectedSnippet(TemplateFormat templateFormat) { - this.templateFormat = templateFormat; - this.snippet = SnippetMatchers.snippet(templateFormat); - } - - @Override - public Statement apply(final Statement base, File outputDirectory, - String operationName) { - this.outputDirectory = outputDirectory; - this.expectedName = operationName; - return new ExpectedSnippetStatement(base); - } - - private void verifySnippet() throws IOException { - if (this.outputDirectory != null && this.expectedName != null) { - File snippetDir = new File(this.outputDirectory, this.expectedName); - File snippetFile = new File(snippetDir, - this.expectedType + "." + this.templateFormat.getFileExtension()); - assertThat(snippetFile, is(this.snippet)); - } - } - - public ExpectedSnippet expectCurlRequest() { - expect("curl-request"); - return this; - } - - public ExpectedSnippet expectHttpieRequest() { - expect("httpie-request"); - return this; - } - - public ExpectedSnippet expectRequestFields() { - expect("request-fields"); - return this; - } - - public ExpectedSnippet expectResponseFields() { - expect("response-fields"); - return this; - } - - public ExpectedSnippet expectRequestHeaders() { - expect("request-headers"); - return this; - } - - public ExpectedSnippet expectResponseHeaders() { - expect("response-headers"); - return this; - } - - public ExpectedSnippet expectLinks() { - expect("links"); - return this; - } - - public ExpectedSnippet expectHttpRequest() { - expect("http-request"); - return this; - } - - public ExpectedSnippet expectHttpResponse() { - expect("http-response"); - return this; - } - - public ExpectedSnippet expectRequestParameters() { - expect("request-parameters"); - return this; - } - - public ExpectedSnippet expectPathParameters() { - expect("path-parameters"); - return this; - } - - public ExpectedSnippet expectRequestParts() { - expect("request-parts"); - return this; - } - - private ExpectedSnippet expect(String type) { - this.expectedType = type; - return this; - } - - public void withContents(Matcher matcher) { - this.snippet.withContents(matcher); - } - - public File getOutputDirectory() { - return this.outputDirectory; - } - - private final class ExpectedSnippetStatement extends Statement { - - private final Statement delegate; - - private ExpectedSnippetStatement(Statement delegate) { - this.delegate = delegate; - } - - @Override - public void evaluate() throws Throwable { - this.delegate.evaluate(); - verifySnippet(); - } - - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java new file mode 100644 index 000000000..f57c5b959 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java @@ -0,0 +1,177 @@ +/* + * Copyright 2014-2016 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.test; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.hamcrest.Matcher; +import org.junit.runners.model.Statement; + +import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.test.SnippetMatchers.SnippetMatcher; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * The {@code ExpectedSnippets} rule is used to verify that a test of a + * {@link TemplatedSnippet} has generated the expected snippets. + * + * @author Andy Wilkinson + * @author Andreas Evers + */ +public class ExpectedSnippets extends OperationTestRule { + + private final TemplateFormat templateFormat; + + private String operationName; + + private File outputDirectory; + + private List expectations = new ArrayList<>(); + + public ExpectedSnippets(TemplateFormat templateFormat) { + this.templateFormat = templateFormat; + } + + @Override + public Statement apply(final Statement base, File outputDirectory, + String operationName) { + this.outputDirectory = outputDirectory; + this.operationName = operationName; + return new ExpectedSnippetsStatement(base); + } + + private void verifySnippets() throws IOException { + if (this.outputDirectory != null && this.operationName != null) { + File snippetDir = new File(this.outputDirectory, this.operationName); + for (ExpectedSnippet expectation : this.expectations) { + expectation.verify(snippetDir); + } + } + } + + public ExpectedSnippet expectCurlRequest() { + return expect("curl-request"); + } + + public ExpectedSnippet expectHttpieRequest() { + return expect("httpie-request"); + } + + public ExpectedSnippet expectRequestFields() { + return expect("request-fields"); + } + + public ExpectedSnippet expectResponseFields() { + return expect("response-fields"); + } + + public ExpectedSnippet expectRequestHeaders() { + return expect("request-headers"); + } + + public ExpectedSnippet expectResponseHeaders() { + return expect("response-headers"); + } + + public ExpectedSnippet expectLinks() { + return expect("links"); + } + + public ExpectedSnippet expectHttpRequest() { + return expect("http-request"); + } + + public ExpectedSnippet expectHttpResponse() { + return expect("http-response"); + } + + public ExpectedSnippet expectRequestParameters() { + return expect("request-parameters"); + } + + public ExpectedSnippet expectPathParameters() { + return expect("path-parameters"); + } + + public ExpectedSnippet expectRequestParts() { + return expect("request-parts"); + } + + public ExpectedSnippet expect(String type) { + ExpectedSnippet expectedSnippet = new ExpectedSnippet( + SnippetMatchers.snippet(this.templateFormat), type); + this.expectations.add(expectedSnippet); + return expectedSnippet; + } + + public File getOutputDirectory() { + return this.outputDirectory; + } + + public String getOperationName() { + return this.operationName; + } + + /** + * Expecations for a particular snippet. + */ + public final class ExpectedSnippet { + + private final SnippetMatcher snippetMatcher; + + private final String snippetName; + + private ExpectedSnippet(SnippetMatcher snippetMatcher, String snippetName) { + this.snippetMatcher = snippetMatcher; + this.snippetName = snippetName; + } + + private void verify(File snippetDir) { + File snippetFile = new File(snippetDir, this.snippetName + "." + + ExpectedSnippets.this.templateFormat.getFileExtension()); + assertThat(snippetFile, is(this.snippetMatcher)); + } + + public void withContents(Matcher matcher) { + this.snippetMatcher.withContents(matcher); + } + + } + + private final class ExpectedSnippetsStatement extends Statement { + + private final Statement delegate; + + private ExpectedSnippetsStatement(Statement delegate) { + this.delegate = delegate; + } + + @Override + public void evaluate() throws Throwable { + this.delegate.evaluate(); + verifySnippets(); + } + + } + +} diff --git a/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/multiple-snippets.snippet b/spring-restdocs-core/src/test/resources/org/springframework/restdocs/templates/multiple-snippets.snippet new file mode 100644 index 000000000..e69de29bb From 13e745a9a93e018b95285de2b3f8e3d5f81a6e90 Mon Sep 17 00:00:00 2001 From: Mathieu POUSSE Date: Thu, 23 Jun 2016 17:28:26 +0200 Subject: [PATCH 191/898] Add support for documenting request part payload fields See gh-270 --- .../docs/asciidoc/documenting-your-api.adoc | 6 + .../payload/PayloadDocumentation.java | 50 +++++ .../payload/RequestPartFieldsSnippet.java | 191 ++++++++++++++++++ ...RequestPartsFieldsSnippetFailureTests.java | 82 ++++++++ .../RequestPartsFieldsSnippetTests.java | 56 +++++ 5 files changed, 385 insertions(+) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index d1a27442b..96117ffda 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -575,6 +575,12 @@ prevent it from appearing in the generated snippet while avoiding the failure de above. +[[documenting-your-api-request-parts]] +=== Request parts content + +If you need to document the content of request part that holds Json data, you should use`requestPartsFields`. +It acts exactly as [[documenting-your-api-request-parts]] but in order to use it, you must specify the part name. + [[documenting-your-api-http-headers]] === HTTP headers diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index c4a57c254..fed726f1d 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -148,6 +148,56 @@ public static RequestFieldsSnippet requestFields(List descripto return new RequestFieldsSnippet(descriptors); } + /** + * Returns a {@code Snippet} that will document the fields of the specified {@code part} + * of the API operations's request payload. The fields will be documented using the + * given {@code descriptors}. + *

          + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, FieldDescriptor... descriptors) { + return requestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified {@code part} + * of the API operations's request payload. The fields will be documented using the given + * {@code descriptors}. + *

          + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors); + } + /** * Returns a {@code Snippet} that will document the fields of the API operations's * request payload. The fields will be documented using the given {@code descriptors}. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java new file mode 100644 index 000000000..4de8f8486 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -0,0 +1,191 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the fields in a request. + * + * @author Mathieu Pousse + * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) + */ +public class RequestPartFieldsSnippet extends AbstractFieldsSnippet { + + private final String partName; + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the + * request part using the given {@code descriptors}. Undocumented fields will trigger a + * failure. + * + * @param partName the part name + * @param descriptors the descriptors + */ + protected RequestPartFieldsSnippet(String partName, List descriptors) { + this(partName, descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the + * request part using the given {@code descriptors}. If {@code ignoreUndocumentedFields} is + * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * + * @param partName the part name + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + boolean ignoreUndocumentedFields) { + this(partName, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. Undocumented fields will trigger a + * failure. + * + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + Map attributes) { + this(partName, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * request using the given {@code descriptors}. The given {@code attributes} will be + * included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + super("request", descriptors, attributes, ignoreUndocumentedFields); + this.partName = partName; + } + + @Override + protected MediaType getContentType(Operation operation) { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate.getHeaders().getContentType(); + } + } + throw new SnippetException(missingPartErrorMessage()); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate.getContent(); + } + } + throw new SnippetException(missingPartErrorMessage()); + } + + /** + * Prepare the error message because the requested part was not found. + * + * @return see description + */ + protected String missingPartErrorMessage() { + return "Request parts with the following names were not found in the request: " + this.partName; + } + + /** + * Returns a new {@code RequestPartFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestPartFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. + * + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet and(List additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, + FieldDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getFieldDescriptors()); + combinedDescriptors.addAll( + PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getFieldDescriptors()); + combinedDescriptors.addAll( + PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java new file mode 100644 index 000000000..a025514ce --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2014-2016 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 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.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.test.ExpectedSnippet; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for failures when rendering {@link RequestPartFieldsSnippet} due to missing or + * undocumented fields. + * + * @author Mathieu Pousse + */ +public class RequestPartsFieldsSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedRequestPartField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); + new RequestPartFieldsSnippet("part", Collections.emptyList()) + .document(new OperationBuilder("undocumented-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); + } + + @Test + public void missingRequestPartField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(startsWith( + "The following parts of the payload were not" + " documented:")); + new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("b").description("one"))) + .document(new OperationBuilder("undocumented-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); + } + + @Test + public void missingRequestPart() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Request parts with the following names were not found in the request: another")); + new RequestPartFieldsSnippet("another", Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(new OperationBuilder("missing-request-fields", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("part", "{\"a\": {\"b\": 5}}".getBytes()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java new file mode 100644 index 000000000..8dbff585d --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.util.Arrays; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateFormat; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for {@link RequestPartFieldsSnippet}. + * + * @author Mathieu Pousse + */ +public class RequestPartsFieldsSnippetTests extends AbstractSnippetTests { + + public RequestPartsFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void mapRequestWithFields() throws IOException { + this.snippet.expectRequestFields("map-request-parts-with-fields") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + + new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two"), + fieldWithPath("a").description("three"))) + .document(operationBuilder("map-request-parts-with-fields") + .request("http://localhost") + .part("part", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + } + +} From d2a5b38c83609e83c405e7b99a49e39fa7d8038c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Oct 2016 12:04:43 +0100 Subject: [PATCH 192/898] Polish "Add support for documenting request part payload fields" - Rebase on latest code, and make use of new support for the same TemplatedSnippet producing multiple snippets with different names from the same template - Expand the documentation - Apply code formatting - Add support for relaxed documentation of a request part's fields Closes gh-270 --- .../docs/asciidoc/documenting-your-api.adoc | 56 ++++- .../example/mockmvc/RequestPartPayload.java | 48 ++++ .../restassured/RequestPartPayload.java | 48 ++++ .../payload/AbstractFieldsSnippet.java | 31 ++- .../payload/PayloadDocumentation.java | 234 ++++++++++++++---- .../payload/RequestPartFieldsSnippet.java | 87 ++++--- .../default-request-part-fields.snippet | 10 + .../default-request-part-fields.snippet | 5 + ...RequestPartFieldsSnippetFailureTests.java} | 39 +-- .../RequestPartFieldsSnippetTests.java | 118 +++++++++ .../RequestPartsFieldsSnippetTests.java | 56 ----- .../restdocs/test/ExpectedSnippets.java | 4 + 12 files changed, 557 insertions(+), 179 deletions(-) create mode 100644 docs/src/test/java/com/example/mockmvc/RequestPartPayload.java create mode 100644 docs/src/test/java/com/example/restassured/RequestPartPayload.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet rename spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/{RequestPartsFieldsSnippetFailureTests.java => RequestPartFieldsSnippetFailureTests.java} (62%) create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java delete mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 96117ffda..4b3d58fac 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -415,7 +415,7 @@ include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request- ---- <1> Perform a `GET` request with two parameters, `page` and `per_page` in the query string. -<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. +<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. Uses the static `requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the `page` parameter. Uses the static `parameterWithName` method on @@ -575,12 +575,58 @@ prevent it from appearing in the generated snippet while avoiding the failure de above. -[[documenting-your-api-request-parts]] -=== Request parts content +[[documenting-your-api-request-parts-payloads]] +=== Request part payloads + +The payload of a request part can be documented in much the same way as the +<>: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=payload] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload + of the request part named `metadata`. Uses the static `requestPartFields` method on + `PayloadDocumentation`. + payload. +<2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=payload] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload + of the request part named `metadata`. Uses the static `requestPartFields` method on + `PayloadDocumentation`. +<2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. + +The result is a snippet that contains a table describing the part's fields. This snippet +is named `request-part-${part-name}-fields.adoc`. For example, documenting a part named +`metadata` will produce a snippet named `request-part-metadata-fields.adoc`. + +When documenting fields, the test will fail if an undocumented field is found in the +payload of the part. Similarly, the test will also fail if a documented field is not found +in the payload of the part and the field has not been marked as optional. For payloads +with a hierarchical structure, documenting a field is sufficient for all of its +descendants to also be treated as having been documented. -If you need to document the content of request part that holds Json data, you should use`requestPartsFields`. -It acts exactly as [[documenting-your-api-request-parts]] but in order to use it, you must specify the part name. +If you do not want to document a field, you can mark it as ignored. This will prevent it +from appearing in the generated snippet while avoiding the failure described above. + +Fields can also be documented in a relaxed mode where any undocumented fields will not +cause a test failure. To do so, use the `relaxedRequestPartFields` method on +`org.springframework.restdocs.payload.PayloadDocumentation`. This can be useful when +documenting a particular scenario where you only want to focus on a subset of the payload +of the part. +For further information on describing fields, documenting payloads that use XML, +and more please refer to the +<>. [[documenting-your-api-http-headers]] === HTTP headers diff --git a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java new file mode 100644 index 000000000..56d6ae85d --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2016 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 com.example.mockmvc; + +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.fileUpload; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class RequestPartPayload { + + private MockMvc mockMvc; + + public void response() throws Exception { + // tag::payload[] + MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", + "<>".getBytes()); + MockMultipartFile metadata = new MockMultipartFile("metadata", "", + "application/json", "{ \"version\": \"1.0\"}".getBytes()); + + this.mockMvc.perform(fileUpload("/images").file(image).file(metadata) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("image-upload", requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))); // <2> + // end::payload[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java new file mode 100644 index 000000000..5dc8eda54 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestPartPayload.java @@ -0,0 +1,48 @@ +/* + * Copyright 2014-2016 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 com.example.restassured; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestPartPayload { + + private RequestSpecification spec; + + public void response() throws Exception { + // tag::payload[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))) // <2> + .when().multiPart("image", new File("image.png"), "image/png") + .multiPart("metadata", metadata).post("images") + .then().assertThat().statusCode(is(200)); + // end::payload[] + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index 16f7ba61b..ae6fb3fe8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -65,10 +65,11 @@ protected AbstractFieldsSnippet(String type, List descriptors, /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named - * {@code -fields}. The fields will be documented using the given - * {@code descriptors} and the given {@code attributes} will be included in the model - * during template rendering. If {@code ignoreUndocumentedFields} is {@code true}, - * undocumented fields will be ignored and will not trigger a failure. + * {@code -fields} using a template named {@code -fields}. The fields will + * be documented using the given {@code descriptors} and the given {@code attributes} + * will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. * * @param type the type of the fields * @param descriptors the field descriptors @@ -77,7 +78,27 @@ protected AbstractFieldsSnippet(String type, List descriptors, */ protected AbstractFieldsSnippet(String type, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super(type + "-fields", attributes); + this(type, type, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields will + * be documented using the given {@code descriptors} and the given {@code attributes} + * will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param name the name of the snippet + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected AbstractFieldsSnippet(String name, String type, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + super(name + "-fields", type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index fed726f1d..2e1d8ea2f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -148,56 +148,6 @@ public static RequestFieldsSnippet requestFields(List descripto return new RequestFieldsSnippet(descriptors); } - /** - * Returns a {@code Snippet} that will document the fields of the specified {@code part} - * of the API operations's request payload. The fields will be documented using the - * given {@code descriptors}. - *

          - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * - * @param part the part name - * @param descriptors the descriptions of the request payload's fields - * @return the snippet that will document the fields - * @see #fieldWithPath(String) - */ - public static RequestPartFieldsSnippet requestPartFields(String part, FieldDescriptor... descriptors) { - return requestPartFields(part, Arrays.asList(descriptors)); - } - - /** - * Returns a {@code Snippet} that will document the fields of the specified {@code part} - * of the API operations's request payload. The fields will be documented using the given - * {@code descriptors}. - *

          - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. - * - * @param part the part name - * @param descriptors the descriptions of the request payload's fields - * @return the snippet that will document the fields - * @see #fieldWithPath(String) - */ - public static RequestPartFieldsSnippet requestPartFields(String part, List descriptors) { - return new RequestPartFieldsSnippet(part, descriptors); - } - /** * Returns a {@code Snippet} that will document the fields of the API operations's * request payload. The fields will be documented using the given {@code descriptors}. @@ -318,6 +268,190 @@ public static RequestFieldsSnippet relaxedRequestFields( return new RequestFieldsSnippet(descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

          + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + FieldDescriptor... descriptors) { + return requestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

          + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

          + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + Map attributes, FieldDescriptor... descriptors) { + return requestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

          + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + Map attributes, FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java index 4de8f8486..f07923deb 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -29,10 +29,12 @@ import org.springframework.restdocs.snippet.SnippetException; /** - * A {@link Snippet} that documents the fields in a request. + * A {@link Snippet} that documents the fields in a request part. * * @author Mathieu Pousse + * @author Andy Wilkinson * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) + * @see PayloadDocumentation#requestPartFields(String, List) */ public class RequestPartFieldsSnippet extends AbstractFieldsSnippet { @@ -40,27 +42,29 @@ public class RequestPartFieldsSnippet extends AbstractFieldsSnippet { /** * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the - * request part using the given {@code descriptors}. Undocumented fields will trigger a - * failure. + * request part using the given {@code descriptors}. Undocumented fields will trigger + * a failure. * - * @param partName the part name + * @param partName the part name * @param descriptors the descriptors */ - protected RequestPartFieldsSnippet(String partName, List descriptors) { + protected RequestPartFieldsSnippet(String partName, + List descriptors) { this(partName, descriptors, null, false); } /** * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the - * request part using the given {@code descriptors}. If {@code ignoreUndocumentedFields} is - * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * request part using the given {@code descriptors}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. * - * @param partName the part name - * @param descriptors the descriptors + * @param partName the part name + * @param descriptors the descriptors * @param ignoreUndocumentedFields whether undocumented fields should be ignored */ protected RequestPartFieldsSnippet(String partName, List descriptors, - boolean ignoreUndocumentedFields) { + boolean ignoreUndocumentedFields) { this(partName, descriptors, null, ignoreUndocumentedFields); } @@ -70,12 +74,12 @@ protected RequestPartFieldsSnippet(String partName, List descri * included in the model during template rendering. Undocumented fields will trigger a * failure. * - * @param partName the part name + * @param partName the part name * @param descriptors the descriptors - * @param attributes the additional attributes + * @param attributes the additional attributes */ protected RequestPartFieldsSnippet(String partName, List descriptors, - Map attributes) { + Map attributes) { this(partName, descriptors, attributes, false); } @@ -86,44 +90,36 @@ protected RequestPartFieldsSnippet(String partName, List descri * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be * ignored and will not trigger a failure. * - * @param partName the part name - * @param descriptors the descriptors - * @param attributes the additional attributes + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes * @param ignoreUndocumentedFields whether undocumented fields should be ignored */ protected RequestPartFieldsSnippet(String partName, List descriptors, - Map attributes, boolean ignoreUndocumentedFields) { - super("request", descriptors, attributes, ignoreUndocumentedFields); + Map attributes, boolean ignoreUndocumentedFields) { + super("request-part-" + partName, "request-part", descriptors, attributes, + ignoreUndocumentedFields); this.partName = partName; } @Override protected MediaType getContentType(Operation operation) { - for (OperationRequestPart candidate : operation.getRequest().getParts()) { - if (candidate.getName().equals(this.partName)) { - return candidate.getHeaders().getContentType(); - } - } - throw new SnippetException(missingPartErrorMessage()); + return findPart(operation).getHeaders().getContentType(); } @Override protected byte[] getContent(Operation operation) throws IOException { + return findPart(operation).getContent(); + } + + private OperationRequestPart findPart(Operation operation) { for (OperationRequestPart candidate : operation.getRequest().getParts()) { if (candidate.getName().equals(this.partName)) { - return candidate.getContent(); + return candidate; } } - throw new SnippetException(missingPartErrorMessage()); - } - - /** - * Prepare the error message because the requested part was not found. - * - * @return see description - */ - protected String missingPartErrorMessage() { - return "Request parts with the following names were not found in the request: " + this.partName; + throw new SnippetException("A request part named '" + this.partName + + "' was not found in the request"); } /** @@ -146,7 +142,8 @@ public final RequestPartFieldsSnippet and(FieldDescriptor... additionalDescripto * @param additionalDescriptors the additional descriptors * @return the new snippet */ - public final RequestPartFieldsSnippet and(List additionalDescriptors) { + public final RequestPartFieldsSnippet and( + List additionalDescriptors) { return andWithPrefix("", additionalDescriptors); } @@ -156,17 +153,18 @@ public final RequestPartFieldsSnippet and(List additionalDescri * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path * of each additional descriptor. * - * @param pathPrefix the prefix to apply to the additional descriptors + * @param pathPrefix the prefix to apply to the additional descriptors * @param additionalDescriptors the additional descriptors * @return the new snippet */ public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, - FieldDescriptor... additionalDescriptors) { + FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); - return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, + Arrays.asList(additionalDescriptors))); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, + this.getAttributes()); } /** @@ -175,17 +173,18 @@ public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path * of each additional descriptor. * - * @param pathPrefix the prefix to apply to the additional descriptors + * @param pathPrefix the prefix to apply to the additional descriptors * @param additionalDescriptors the additional descriptors * @return the new snippet */ public final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, - List additionalDescriptors) { + List additionalDescriptors) { List combinedDescriptors = new ArrayList<>( getFieldDescriptors()); combinedDescriptors.addAll( PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); - return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, this.getAttributes()); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, + this.getAttributes()); } } diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet new file mode 100644 index 000000000..46cd43fe4 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} +|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet new file mode 100644 index 000000000..27a4e4379 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet @@ -0,0 +1,5 @@ +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +`{{path}}` | `{{type}}` | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java similarity index 62% rename from spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java rename to spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java index a025514ce..03cf666ec 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java @@ -26,7 +26,6 @@ import org.springframework.restdocs.snippet.SnippetException; import org.springframework.restdocs.templates.TemplateFormats; -import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.restdocs.test.OperationBuilder; import static org.hamcrest.CoreMatchers.equalTo; @@ -38,11 +37,13 @@ * undocumented fields. * * @author Mathieu Pousse + * @author Andy Wilkinson */ -public class RequestPartsFieldsSnippetFailureTests { +public class RequestPartFieldsSnippetFailureTests { @Rule - public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor()); + public OperationBuilder operationBuilder = new OperationBuilder( + TemplateFormats.asciidoctor()); @Rule public ExpectedException thrown = ExpectedException.none(); @@ -50,33 +51,33 @@ public class RequestPartsFieldsSnippetFailureTests { @Test public void undocumentedRequestPartField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); new RequestPartFieldsSnippet("part", Collections.emptyList()) - .document(new OperationBuilder("undocumented-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("part", "{\"a\": 5}".getBytes()).build()); + .document(this.operationBuilder.request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); } @Test public void missingRequestPartField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); - new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("b").description("one"))) - .document(new OperationBuilder("undocumented-request-field", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("part", "{\"a\": 5}".getBytes()).build()); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); + new RequestPartFieldsSnippet("part", + Arrays.asList(fieldWithPath("b").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); } @Test public void missingRequestPart() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(equalTo("Request parts with the following names were not found in the request: another")); - new RequestPartFieldsSnippet("another", Arrays.asList(fieldWithPath("a.b").description("one"))) - .document(new OperationBuilder("missing-request-fields", - this.snippet.getOutputDirectory()).request("http://localhost") - .part("part", "{\"a\": {\"b\": 5}}".getBytes()).build()); + this.thrown.expectMessage( + equalTo("A request part named 'another' was not found in the request")); + new RequestPartFieldsSnippet("another", + Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .part("part", "{\"a\": {\"b\": 5}}".getBytes()).build()); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java new file mode 100644 index 000000000..0d4c7c32c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.templates.TemplateFormat; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for {@link RequestPartFieldsSnippet}. + * + * @author Mathieu Pousse + * @author Andy Wilkinson + */ +public class RequestPartFieldsSnippetTests extends AbstractSnippetTests { + + public RequestPartFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void mapRequestPartFields() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + + new RequestPartFieldsSnippet("one", + Arrays.asList(fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two"), fieldWithPath("a") + .description("three"))) + .document( + this.operationBuilder + .request("http://localhost") + .part("one", + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}" + .getBytes()) + .build()); + } + + @Test + public void multipleRequestParts() throws IOException { + this.snippets.expectRequestPartFields("one"); + this.snippets.expectRequestPartFields("two"); + Operation operation = this.operationBuilder.request("http://localhost") + .part("one", "{}".getBytes()).and().part("two", "{}".getBytes()).build(); + new RequestPartFieldsSnippet("one", Collections.emptyList()) + .document(operation); + new RequestPartFieldsSnippet("two", Collections.emptyList()) + .document(operation); + } + + @Test + public void allUndocumentedRequestPartFieldsCanBeIgnored() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); + new RequestPartFieldsSnippet("one", + Arrays.asList(fieldWithPath("b").description("Field b")), true) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\": 5, \"b\": 4}".getBytes()).build()); + } + + @Test + public void additionalDescriptors() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + + PayloadDocumentation + .requestPartFields("one", fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two")) + .and(fieldWithPath("a").description("three")) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + } + + @Test + public void prefixedAdditionalDescriptors() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); + + PayloadDocumentation + .requestPartFields("one", fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), + fieldWithPath("c").description("three")) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java deleted file mode 100644 index 8dbff585d..000000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2014-2016 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 java.io.IOException; -import java.util.Arrays; - -import org.junit.Test; - -import org.springframework.restdocs.AbstractSnippetTests; -import org.springframework.restdocs.templates.TemplateFormat; - -import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; - -/** - * Tests for {@link RequestPartFieldsSnippet}. - * - * @author Mathieu Pousse - */ -public class RequestPartsFieldsSnippetTests extends AbstractSnippetTests { - - public RequestPartsFieldsSnippetTests(String name, TemplateFormat templateFormat) { - super(name, templateFormat); - } - - @Test - public void mapRequestWithFields() throws IOException { - this.snippet.expectRequestFields("map-request-parts-with-fields") - .withContents(tableWithHeader("Path", "Type", "Description") - .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") - .row("`a`", "`Object`", "three")); - - new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("a.b").description("one"), - fieldWithPath("a.c").description("two"), - fieldWithPath("a").description("three"))) - .document(operationBuilder("map-request-parts-with-fields") - .request("http://localhost") - .part("part", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) - .build()); - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java index f57c5b959..3a174532f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java @@ -81,6 +81,10 @@ public ExpectedSnippet expectRequestFields() { return expect("request-fields"); } + public ExpectedSnippet expectRequestPartFields(String partName) { + return expect("request-part-" + partName + "-fields"); + } + public ExpectedSnippet expectResponseFields() { return expect("response-fields"); } From 60211dab868767711a47dc3f4ad5e19cad213869 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 25 Oct 2016 16:10:49 +0100 Subject: [PATCH 193/898] Polishing - Pass attributes into RequestPartFieldsSnippet - Add missing package.info.java for new asciidoctor package --- .../restdocs/asciidoctor/package-info.java | 20 +++++++++++++++++++ .../payload/PayloadDocumentation.java | 8 ++++---- 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java diff --git a/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java new file mode 100644 index 000000000..45e050e7f --- /dev/null +++ b/spring-restdocs-asciidoctor/src/main/java/org/springframework/restdocs/asciidoctor/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014-2016 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. + */ + +/** + * Spring REST Docs Asciidoctor extensions. + */ +package org.springframework.restdocs.asciidoctor; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 2e1d8ea2f..bb6bbbc28 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -381,7 +381,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, */ public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, FieldDescriptor... descriptors) { - return requestPartFields(part, Arrays.asList(descriptors)); + return requestPartFields(part, attributes, Arrays.asList(descriptors)); } /** @@ -409,7 +409,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, */ public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, List descriptors) { - return new RequestPartFieldsSnippet(part, descriptors); + return new RequestPartFieldsSnippet(part, descriptors, attributes); } /** @@ -429,7 +429,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, FieldDescriptor... descriptors) { - return relaxedRequestPartFields(part, Arrays.asList(descriptors)); + return relaxedRequestPartFields(part, attributes, Arrays.asList(descriptors)); } /** @@ -449,7 +449,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, List descriptors) { - return new RequestPartFieldsSnippet(part, descriptors, true); + return new RequestPartFieldsSnippet(part, descriptors, attributes, true); } /** From 7bcfbd9e35ebcb025ceb67876362934c0289dc16 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 27 Oct 2016 10:16:46 +0100 Subject: [PATCH 194/898] Add support for documenting a portion of a request or response payload Closes gh-312 --- .../docs/asciidoc/documenting-your-api.adoc | 63 ++ docs/src/test/java/com/example/Payload.java | 10 + .../java/com/example/mockmvc/Payload.java | 23 +- .../java/com/example/restassured/Payload.java | 12 + .../payload/AbstractFieldsSnippet.java | 91 ++- .../FieldPathPayloadSubsectionExtractor.java | 108 +++ .../payload/PayloadDocumentation.java | 675 ++++++++++++++++++ .../payload/PayloadHandlingException.java | 15 +- .../payload/PayloadSubsectionExtractor.java | 55 ++ .../payload/RequestFieldsSnippet.java | 73 +- .../payload/RequestPartFieldsSnippet.java | 76 +- .../payload/ResponseFieldsSnippet.java | 76 +- ...ldPathPayloadSubsectionExtractorTests.java | 110 +++ .../payload/RequestFieldsSnippetTests.java | 28 + .../RequestPartFieldsSnippetTests.java | 20 + .../payload/ResponseFieldsSnippetTests.java | 14 + 16 files changed, 1421 insertions(+), 28 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 4b3d58fac..557a13d26 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -402,6 +402,69 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] <1> Document the array <2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` +[[documenting-your-api-request-response-payloads-subsections]] +==== Documenting a portion of a request or response payload + +If a payload is large or structurally complex, it can be useful to document +individual sections of the payload. REST Docs allows you to do so by extracting a +subsection of the payload and then documenting it. + +Consider the following JSON response payload: + +[source,json,indent=0] +---- + { + "weather": { + "wind": { + "speed": 15.3, + "direction": 287.0 + }, + "temperature": { + "high": 21.2, + "low": 14.8 + } + } + } +---- + +A snippet that documents the fields of the `temperature` object (`high` and `low`) can +be produced as follows: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload + beneath the path `weather.temperature`. Uses the static `beneathPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] +---- +<1> Produce a snippet describing the fields in the subsection of the response payload + beneath the path `weather.temperature`. Uses the static `beneathPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. +<2> Document the `high` and `low` fields. + +The result is a snippet that contains a table describing the `high` and `low` fields +of `weather.temperature`. To make the snippet's name distinct, an identifier for the +subsection is included. By default, this identifier is `beneath-${path}`. For +example, the code above will result in a snippet named +`response-fields-beneath-weather.temperature.adoc`. The identifier can be customized using +the `withSubsectionId(String)` method: + +---- +include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] +---- + +This example will result in a snippet named `response-fields-temp.adoc`. + + + [[documenting-your-api-request-parameters]] === Request parameters diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index d33de4d16..f553927df 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -18,7 +18,9 @@ import org.springframework.restdocs.payload.FieldDescriptor; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; public class Payload { @@ -31,4 +33,12 @@ public void bookFieldDescriptors() { // end::book-descriptors[] } + public void customSubsectionId() { + // tag::custom-subsection-id[] + responseFields(beneathPath("weather.temperature").withSubsectionId("temp"), + fieldWithPath("high").description("…"), + fieldWithPath("low").description("…")); + // end::custom-subsection-id[] + } + } diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 615354a3f..9701f29f1 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -16,21 +16,22 @@ package com.example.mockmvc; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; + import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; 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.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.springframework.http.MediaType; -import org.springframework.restdocs.payload.FieldDescriptor; -import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.web.servlet.MockMvc; - public class Payload { private MockMvc mockMvc; @@ -91,4 +92,14 @@ public void descriptorReuse() throws Exception { // end::book-array[] } + public void subsection() throws Exception { + // tag::subsection[] + this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))); + // end::subsection[] + } + } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index 155e357e5..d54a7e6db 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -23,6 +23,7 @@ import com.jayway.restassured.specification.RequestSpecification; import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -95,4 +96,15 @@ public void descriptorReuse() throws Exception { // end::book-array[] } + public void subsection() throws Exception { + // tag::subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> + fieldWithPath("low").description("The forecast low in degrees celcius")))) + .when().get("/locations/1") + .then().assertThat().statusCode(is(200)); + // end::subsection[] + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index ae6fb3fe8..a630cc385 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -45,6 +45,8 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { private final String type; + private final PayloadSubsectionExtractor subsectionExtractor; + /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields}. The fields will be documented using the given @@ -81,6 +83,29 @@ protected AbstractFieldsSnippet(String type, List descriptors, this(type, type, descriptors, attributes, ignoreUndocumentedFields); } + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields in + * the subsection of the payload extracted by the given {@code subsectionExtractor} + * will be documented using the given {@code descriptors} and the given + * {@code attributes} will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @param subsectionExtractor the subsection extractor + * @since 1.2.0 + */ + protected AbstractFieldsSnippet(String type, List descriptors, + Map attributes, boolean ignoreUndocumentedFields, + PayloadSubsectionExtractor subsectionExtractor) { + this(type, type, descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); + } + /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named * {@code -fields} using a template named {@code -fields}. The fields will @@ -98,7 +123,35 @@ protected AbstractFieldsSnippet(String type, List descriptors, protected AbstractFieldsSnippet(String name, String type, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super(name + "-fields", type + "-fields", attributes); + this(name, type, descriptors, attributes, ignoreUndocumentedFields, null); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -fields}. The fields in + * the subsection of the payload identified by {@code subsectionPath} will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param name the name of the snippet + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @param subsectionExtractor the subsection extractor documented. {@code null} or an + * empty string can be used to indicate that the entire payload should be documented. + * @since 1.2.0 + */ + protected AbstractFieldsSnippet(String name, String type, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields, + PayloadSubsectionExtractor subsectionExtractor) { + super(name + "-fields" + + (subsectionExtractor != null + ? "-" + subsectionExtractor.getSubsectionId() : ""), + type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { @@ -112,11 +165,24 @@ protected AbstractFieldsSnippet(String name, String type, this.fieldDescriptors = descriptors; this.ignoreUndocumentedFields = ignoreUndocumentedFields; this.type = type; + this.subsectionExtractor = subsectionExtractor; } @Override protected Map createModel(Operation operation) { - ContentHandler contentHandler = getContentHandler(operation); + byte[] content; + try { + content = verifyContent(getContent(operation)); + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } + MediaType contentType = getContentType(operation); + if (this.subsectionExtractor != null) { + content = verifyContent( + this.subsectionExtractor.extractSubsection(content, contentType)); + } + ContentHandler contentHandler = getContentHandler(content, contentType); validateFieldDocumentation(contentHandler); @@ -146,28 +212,27 @@ protected Map createModel(Operation operation) { return model; } - private ContentHandler getContentHandler(Operation operation) { - MediaType contentType = getContentType(operation); - ContentHandler contentHandler; + private byte[] verifyContent(byte[] content) { + if (content.length == 0) { + throw new SnippetException("Cannot document " + this.type + " fields as the " + + this.type + " body is empty"); + } + return content; + } + private ContentHandler getContentHandler(byte[] content, MediaType contentType) { try { - byte[] content = getContent(operation); - if (content.length == 0) { - throw new SnippetException("Cannot document " + this.type - + " fields as the " + this.type + " body is empty"); - } if (contentType != null && MediaType.APPLICATION_XML.isCompatibleWith(contentType)) { - contentHandler = new XmlContentHandler(content); + return new XmlContentHandler(content); } else { - contentHandler = new JsonContentHandler(content); + return new JsonContentHandler(content); } } catch (IOException ex) { throw new ModelCreationException(ex); } - return contentHandler; } private void validateFieldDocumentation(ContentHandler payloadHandler) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java new file mode 100644 index 000000000..ad948e820 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractor.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.http.MediaType; + +/** + * A {@link PayloadSubsectionExtractor} that extracts the subsection of the JSON payload + * identified by a field path. + * + * @author Andy Wilkinson + * @since 1.2.0 + * @see PayloadDocumentation#beneathPath(String) + */ +public class FieldPathPayloadSubsectionExtractor + implements PayloadSubsectionExtractor { + + private final String fieldPath; + + private final String subsectionId; + + /** + * Creates a new {@code FieldPathPayloadSubsectionExtractor} that will extract the + * subsection of the JSON payload found at the given {@code fieldPath}. The + * {@code fieldPath} prefixed with {@code beneath-} with be used as the subsection ID. + * + * @param fieldPath the path of the field + */ + protected FieldPathPayloadSubsectionExtractor(String fieldPath) { + this(fieldPath, "beneath-" + fieldPath); + } + + /** + * Creates a new {@code FieldPathPayloadSubsectionExtractor} that will extract the + * subsection of the JSON payload found at the given {@code fieldPath} and that will + * us the given {@code subsectionId} to identify the subsection. + * + * @param fieldPath the path of the field + * @param subsectionId the ID of the subsection + */ + protected FieldPathPayloadSubsectionExtractor(String fieldPath, String subsectionId) { + this.fieldPath = fieldPath; + this.subsectionId = subsectionId; + } + + @Override + public byte[] extractSubsection(byte[] payload, MediaType contentType) { + ObjectMapper objectMapper = new ObjectMapper(); + try { + JsonFieldPath compiledPath = JsonFieldPath.compile(this.fieldPath); + Object extracted = new JsonFieldProcessor().extract(compiledPath, + objectMapper.readValue(payload, Object.class)); + if (extracted instanceof List && !compiledPath.isPrecise()) { + List extractedList = (List) extracted; + if (extractedList.size() == 1) { + extracted = extractedList.get(0); + } + else { + throw new PayloadHandlingException(this.fieldPath + + " does not uniquely identify a subsection of the payload"); + } + } + return objectMapper.writeValueAsBytes(extracted); + } + catch (IOException ex) { + throw new PayloadHandlingException(ex); + } + } + + @Override + public String getSubsectionId() { + return this.subsectionId; + } + + /** + * Returns the path of the field that will be extracted. + * + * @return the path of the field + */ + protected String getFieldPath() { + return this.fieldPath; + } + + @Override + public FieldPathPayloadSubsectionExtractor withSubsectionId(String subsectionId) { + return new FieldPathPayloadSubsectionExtractor(this.fieldPath, subsectionId); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index bb6bbbc28..286cd185a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -268,6 +268,216 @@ public static RequestFieldsSnippet relaxedRequestFields( return new RequestFieldsSnippet(descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of the subsection of API + * operations's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors}. + *

          + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return requestFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields in the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. + *

          + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedRequestFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operations's request payload extracted by the given {@code subsectionExtractor} + * . The fields will be documented using the given {@code descriptors}. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. + *

          + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * payload, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return requestFields(subsectionExtractor, attributes, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. + *

          + * If a field is present in the request payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * payload, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet requestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedRequestFields(subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the subsection of the + * API operation's request payload extracted by the given {@code subsectionExtractor}. + * The fields will be documented using the given {@code descriptors} and the given + * {@code attributes} will be available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestFieldsSnippet relaxedRequestFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestFieldsSnippet(subsectionExtractor, descriptors, attributes, + true); + } + /** * Returns a {@code Snippet} that will document the fields of the specified * {@code part} of the API operations's request payload. The fields will be documented @@ -287,6 +497,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @param part the part name * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -452,6 +663,235 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, return new RequestPartFieldsSnippet(part, descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

          + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the subsection's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return requestPartFields(part, subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

          + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the subsection's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, subsectionExtractor, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors}. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the givne {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

          + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return requestPartFields(part, subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

          + * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, + attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the + * specified {@code part} of the API operations's request payload. The subsection will + * be extracted by the given {@code subsectionExtractor}. The fields will be + * documented using the given {@code descriptors} and the given {@code attributes} + * will be available during snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, subsectionExtractor, descriptors, + attributes, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} @@ -494,7 +934,9 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( List descriptors) { @@ -511,7 +953,9 @@ public static ResponseFieldsSnippet responseFields( * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields + * @since 1.2.0 * @see #fieldWithPath(String) + * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( FieldDescriptor... descriptors) { @@ -623,6 +1067,225 @@ public static ResponseFieldsSnippet relaxedResponseFields( return new ResponseFieldsSnippet(descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

          + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return responseFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

          + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + FieldDescriptor... descriptors) { + return relaxedResponseFields(subsectionExtractor, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} . + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

          + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return responseFields(subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

          + * If a field is present in the response payload, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the response + * payload, 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. + *

          + * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet responseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, attributes); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, FieldDescriptor... descriptors) { + return relaxedResponseFields(subsectionExtractor, attributes, + Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of a subsection of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The fields will be documented using the given + * {@code descriptors} and the given {@code attributes} will be available during + * snippet generation. + *

          + * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @param descriptors the descriptions of the response payload's fields + * @return the snippet that will document the fields + * @since 1.2.0 + * @see #fieldWithPath(String) + * @see #beneathPath(String) + */ + public static ResponseFieldsSnippet relaxedResponseFields( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes, List descriptors) { + return new ResponseFieldsSnippet(subsectionExtractor, descriptors, attributes, + true); + } + /** * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} * applied to their paths. @@ -651,6 +1314,18 @@ public static List applyPathPrefix(String pathPrefix, return prefixedDescriptors; } + /** + * Returns a {@link PayloadSubsectionExtractor} that will extract the subsection of + * the JSON payload found beneath the given {@code path}. + * + * @param path the path + * @return the subsection extractor + * @since 1.2.0 + */ + public static PayloadSubsectionExtractor beneathPath(String path) { + return new FieldPathPayloadSubsectionExtractor(path); + } + private static Attribute[] asArray(Map attributeMap) { List attributes = new ArrayList<>(); for (Map.Entry attribute : attributeMap.entrySet()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java index 383ba2221..6619f5ae5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadHandlingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -26,7 +26,18 @@ class PayloadHandlingException extends RuntimeException { /** - * Creates a new {@code PayloadHandlingException} with the given cause. + * Creates a new {@code PayloadHandlingException} with the given {@code message}. + * + * @param message the message + * @since 1.2.0 + */ + PayloadHandlingException(String message) { + super(message); + } + + /** + * Creates a new {@code PayloadHandlingException} with the given {@code cause}. + * * @param cause the cause of the failure */ PayloadHandlingException(Throwable cause) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java new file mode 100644 index 000000000..c69989baa --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadSubsectionExtractor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014-2016 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 org.springframework.http.MediaType; + +/** + * Strategy interface for extracting a subsection of a payload. + * + * @param The subsection extractor subclass + * @author Andy Wilkinson + * @since 1.2.0 + */ +public interface PayloadSubsectionExtractor> { + + /** + * Extracts a subsection of the given {@code payload} that has the given + * {@code contentType}. + * + * @param payload the payload + * @param contentType the content type of the payload + * @return the subsection of the payload + */ + byte[] extractSubsection(byte[] payload, MediaType contentType); + + /** + * Returns an identifier for the subsection that this extractor will extract. + * + * @return the identifier + */ + String getSubsectionId(); + + /** + * Returns an extractor with the given {@code subsectionId}. + * + * @param subsectionId the subsection ID + * @return the customized extractor + */ + T withSubsectionId(String subsectionId); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index ea8f68b2c..9be9e32c5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -86,7 +86,74 @@ protected RequestFieldsSnippet(List descriptors, */ protected RequestFieldsSnippet(List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super("request", descriptors, attributes, ignoreUndocumentedFields); + this(null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. Undocumented fields will trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. If {@code ignoreUndocumentedFields} is {@code true}, + * undocumented fields will be ignored and will not trigger a failure. + * + * @param subsectionExtractor the subsection extractor document + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. The given {@code attributes} will be included in the + * model during template rendering. Undocumented fields will trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestFieldsSnippet} that will document the fields in the + * subsection of the request extracted by the given {@code subsectionExtractor} using + * the given {@code descriptors}. The given {@code attributes} will be included in the + * model during template rendering. If {@code ignoreUndocumentedFields} is + * {@code true}, undocumented fields will be ignored and will not trigger a failure. + * + * @param subsectionExtractor the path identifying the subsection of the payload to + * document + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected RequestFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + super("request", descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); } @Override @@ -137,8 +204,8 @@ public final RequestFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, + Arrays.asList(additionalDescriptors))); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java index f07923deb..1eb5205db 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -33,6 +33,7 @@ * * @author Mathieu Pousse * @author Andy Wilkinson + * @since 1.2.0 * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) * @see PayloadDocumentation#requestPartFields(String, List) */ @@ -97,8 +98,81 @@ protected RequestPartFieldsSnippet(String partName, List descri */ protected RequestPartFieldsSnippet(String partName, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { + this(partName, null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. Undocumented fields + * will trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(partName, subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(partName, subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. + * Undocumented fields will trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(partName, subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in a + * subsection of the request part using the given {@code descriptors}. The subsection + * will be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param partName the part name + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { super("request-part-" + partName, "request-part", descriptors, attributes, - ignoreUndocumentedFields); + ignoreUndocumentedFields, subsectionExtractor); this.partName = partName; } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index cabe29484..79d306ea8 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -87,7 +87,77 @@ protected ResponseFieldsSnippet(List descriptors, */ protected ResponseFieldsSnippet(List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super("response", descriptors, attributes, ignoreUndocumentedFields); + this(null, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. Undocumented fields will + * trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors) { + this(subsectionExtractor, descriptors, null, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in the + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, boolean ignoreUndocumentedFields) { + this(subsectionExtractor, descriptors, null, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. + * Undocumented fields will trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes) { + this(subsectionExtractor, descriptors, attributes, false); + } + + /** + * Creates a new {@code ResponseFieldsSnippet} that will document the fields in a + * subsection of the response using the given {@code descriptors}. The subsection will + * be extracted using the given {@code subsectionExtractor}. The given + * {@code attributes} will be included in the model during template rendering. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param subsectionExtractor the subsection extractor + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + * @since 1.2.0 + */ + protected ResponseFieldsSnippet(PayloadSubsectionExtractor subsectionExtractor, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + super("response", descriptors, attributes, ignoreUndocumentedFields, + subsectionExtractor); } @Override @@ -138,8 +208,8 @@ public final ResponseFieldsSnippet andWithPrefix(String pathPrefix, FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll( - PayloadDocumentation.applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, + Arrays.asList(additionalDescriptors))); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java new file mode 100644 index 000000000..eabc887a4 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/FieldPathPayloadSubsectionExtractorTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.http.MediaType; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link FieldPathPayloadSubsectionExtractor}. + * + * @author Andy Wilkinson + */ +public class FieldPathPayloadSubsectionExtractorTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + @SuppressWarnings("unchecked") + public void extractMapSubsectionOfJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.b") + .extractSubsection("{\"a\":{\"b\":{\"c\":5}}}".getBytes(), + MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, + Map.class); + assertThat(extracted.size(), is(equalTo(1))); + assertThat(extracted.get("c"), is(equalTo((Object) 5))); + } + + @Test + @SuppressWarnings("unchecked") + public void extractMultiElementArraySubsectionOfJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a") + .extractSubsection("{\"a\":[{\"b\":5},{\"b\":4}]}".getBytes(), + MediaType.APPLICATION_JSON); + List> extracted = new ObjectMapper() + .readValue(extractedPayload, List.class); + assertThat(extracted.size(), is(equalTo(2))); + assertThat(extracted.get(0).get("b"), is(equalTo((Object) 5))); + assertThat(extracted.get(1).get("b"), is(equalTo((Object) 4))); + } + + @Test + @SuppressWarnings("unchecked") + public void extractSingleElementArraySubsectionOfJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[]") + .extractSubsection("{\"a\":[{\"b\":5}]}".getBytes(), + MediaType.APPLICATION_JSON); + List> extracted = new ObjectMapper() + .readValue(extractedPayload, List.class); + assertThat(extracted.size(), is(equalTo(1))); + assertThat(extracted.get(0).get("b"), is(equalTo((Object) 5))); + } + + @Test + @SuppressWarnings("unchecked") + public void extractMapSubsectionFromSingleElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + byte[] extractedPayload = new FieldPathPayloadSubsectionExtractor("a.[].b") + .extractSubsection("{\"a\":[{\"b\":{\"c\":5}}]}".getBytes(), + MediaType.APPLICATION_JSON); + Map extracted = new ObjectMapper().readValue(extractedPayload, + Map.class); + assertThat(extracted.size(), is(equalTo(1))); + assertThat(extracted.get("c"), is(equalTo((Object) 5))); + } + + @Test + public void extractMapSubsectionFromMultiElementArrayInAJsonMap() + throws JsonParseException, JsonMappingException, IOException { + this.thrown.expect(PayloadHandlingException.class); + this.thrown.expectMessage( + equalTo("a.[].b does not uniquely identify a subsection of the payload")); + new FieldPathPayloadSubsectionExtractor("a.[].b").extractSubsection( + "{\"a\":[{\"b\":{\"c\":5}},{\"b\":{\"c\":6}}]}".getBytes(), + MediaType.APPLICATION_JSON); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 8e6d8414d..d3ad4e25c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -33,7 +33,9 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -63,6 +65,19 @@ public void mapRequestWithFields() throws IOException { .build()); } + @Test + public void subsectionOfMapRequest() throws IOException { + this.snippets.expect("request-fields-beneath-a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + + requestFields(beneathPath("a"), fieldWithPath("b").description("one"), + fieldWithPath("c").description("two")) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + } + @Test public void arrayRequestWithFields() throws IOException { this.snippets.expectRequestFields() @@ -81,6 +96,19 @@ public void arrayRequestWithFields() throws IOException { .build()); } + @Test + public void subsectionOfArrayRequest() throws IOException { + this.snippets.expect("request-fields-beneath-[].a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + + requestFields(beneathPath("[].a"), fieldWithPath("b").description("one"), + fieldWithPath("c").description("two")) + .document(this.operationBuilder.request("http://localhost") + .content("[{\"a\": {\"b\": 5, \"c\": \"charlie\"}}]") + .build()); + } + @Test public void ignoredRequestField() throws IOException { this.snippets.expectRequestFields() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java index 0d4c7c32c..8a1334eea 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java @@ -26,6 +26,7 @@ import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.templates.TemplateFormat; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; /** @@ -60,6 +61,25 @@ public void mapRequestPartFields() throws IOException { .build()); } + @Test + public void mapRequestPartSubsectionFields() throws IOException { + this.snippets.expect("request-part-one-fields-beneath-a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + + new RequestPartFieldsSnippet("one", + beneathPath("a"), Arrays + .asList(fieldWithPath("b").description("one"), + fieldWithPath("c").description("two"))) + .document( + this.operationBuilder + .request("http://localhost") + .part("one", + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}" + .getBytes()) + .build()); + } + @Test public void multipleRequestParts() throws IOException { this.snippets.expectRequestPartFields("one"); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 59002896a..3b8c17287 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -33,7 +33,9 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -70,6 +72,18 @@ public void mapResponseWithFields() throws IOException { .build()); } + @Test + public void subsectionOfMapResponse() throws IOException { + this.snippets.expect("response-fields-beneath-a") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`b`", "`Number`", "one").row("`c`", "`String`", "two")); + responseFields(beneathPath("a"), fieldWithPath("b").description("one"), + fieldWithPath("c").description("two")) + .document(this.operationBuilder.response() + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + } + @Test public void arrayResponseWithFields() throws IOException { this.snippets.expectResponseFields() From cbd96f301d87078b01c6852723bf0861bdfbca11 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 27 Oct 2016 17:33:34 +0100 Subject: [PATCH 195/898] Update field snippets to no longer document whole subsection by default Previously, when a field was documented it would implicitly document the whole subsection of the payload identified by that field. This could lead to users inadvertently failing to document part of the payload. Arguably, this was a bug as it violated REST Docs' principle of producing accurate, detail documentation. However, fixing it requires a breaking change as people may also be relying on this behaviour. A balance needed to be struck so the fix is being made in a minor release. This commit introduces a new subsectionWithPath method which returns a SubsectionDescriptor; a specialisation of FieldDescriptor. Users that were intentionally relying on the old behaviour will have to replace some usage of fieldWithPath with subsectionWithPath instead. Users who were unintentionally relying on the old behaviour will have to add some additional descriptors produced using fieldWithPath and will receive more accurate documentation in return. Closes gh-274 --- .../docs/asciidoc/documenting-your-api.adoc | 67 ++- .../java/com/example/mockmvc/Payload.java | 20 +- .../java/com/example/restassured/Payload.java | 19 +- .../com/example/ApiDocumentationSpec.groovy | 9 +- .../com/example/notes/ApiDocumentation.java | 19 +- .../com/example/notes/ApiDocumentation.java | 33 +- .../payload/AbstractFieldsSnippet.java | 8 +- .../restdocs/payload/FieldDescriptor.java | 2 +- .../restdocs/payload/JsonContentHandler.java | 11 +- .../restdocs/payload/JsonFieldProcessor.java | 65 ++ .../payload/PayloadDocumentation.java | 553 +++++++++++------- .../payload/SubsectionDescriptor.java | 37 ++ .../restdocs/payload/XmlContentHandler.java | 38 +- .../payload/JsonFieldProcessorTests.java | 65 ++ .../RequestFieldsSnippetFailureTests.java | 18 +- .../payload/RequestFieldsSnippetTests.java | 40 ++ .../payload/XmlContentHandlerTests.java | 91 +++ ...kMvcRestDocumentationIntegrationTests.java | 8 +- ...uredRestDocumentationIntegrationTests.java | 3 +- 19 files changed, 842 insertions(+), 264 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 557a13d26..a1eeb908f 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -110,7 +110,19 @@ include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links] In addition to the hypermedia-specific support <>, support for general documentation of request and response payloads is also -provided. For example: +provided. Consider the following payload: + +[source,json,indent=0] +---- + { + "contact": { + "name": "Jane Doe", + "email": "jane.doe@example.com" + } + } +---- + +It can be documented like this: [source,java,indent=0,role="primary"] .MockMvc @@ -120,9 +132,9 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=response] <1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. To document a request `requestFields` can be used. Both are static methods on `org.springframework.restdocs.payload.PayloadDocumentation`. -<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on - `org.springframework.restdocs.payload.PayloadDocumentation`. -<3> Expect a field with the path `contact.email`. +<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method + on `org.springframework.restdocs.payload.PayloadDocumentation`. +<3> Expect a field with the path `contact.name`. [source,java,indent=0,role="secondary"] .REST Assured @@ -132,9 +144,9 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=response] <1> Configure Spring REST docs to produce a snippet describing the fields in the response payload. To document a request `requestFields` can be used. Both are static methods on `org.springframework.restdocs.payload.PayloadDocumentation`. -<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on +<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method on `org.springframework.restdocs.payload.PayloadDocumentation`. -<3> Expect a field with the path `contact.email`. +<3> Expect a field with the path `contact.name`. The result is a snippet that contains a table describing the fields. For requests this snippet is named `request-fields.adoc`. For responses this snippet is named @@ -142,12 +154,36 @@ snippet is named `request-fields.adoc`. For responses this snippet is named When documenting fields, the test will fail if an undocumented field is found in the payload. Similarly, the test will also fail if a documented field is not found in the -payload and the field has not been marked as optional. For payloads with a hierarchical -structure, documenting a field is sufficient for all of its descendants to also be -treated as having been documented. +payload and the field has not been marked as optional. -If you do not want to document a field, you can mark it as ignored. This will prevent it -from appearing in the generated snippet while avoiding the failure described above. +If you don't want to provide detailed documentation for all of the fields, an entire +subsection of a payload can be documented. For example: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] +---- +<1> Document the subsection with the path `contact`. `contact.email` and `contact.name` + are now seen has having also been documented. Uses the static `subsectionWithPath` + method on `org.springframework.restdocs.payload.PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] +---- +<1> Document the subsection with the path `contact`. `contact.email` and `contact.name` + are now seen has having also been documented. Uses the static `subsectionWithPath` + method on `org.springframework.restdocs.payload.PayloadDocumentation`. + +`subsectionWithPath` can be useful for providing a high-level overview of a particular +section of a payload. Separate, more detailed documentation for a subsection can then +<>. + +If you do not want to document a field or subsection at all, you can mark it as ignored. +This will prevent it from appearing in the generated snippet while avoiding the failure +described above. Fields can also be documented in a relaxed mode where any undocumented fields will not cause a test failure. To do so, use the `relaxedRequestFields` and `relaxedResponseFields` @@ -233,7 +269,7 @@ The following paths are all present: |=== -A response that uses an array at its root can also be documented. The path `[]` will refer +A payload that uses an array at its root can also be documented. The path `[]` will refer to the entire array. You can then use bracket or dot notation to identify fields within the array's entries. For example, `[].id` corresponds to the `id` field of every object found in the following array: @@ -403,7 +439,7 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] <2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` [[documenting-your-api-request-response-payloads-subsections]] -==== Documenting a portion of a request or response payload +==== Documenting a subsection of a request or response payload If a payload is large or structurally complex, it can be useful to document individual sections of the payload. REST Docs allows you to do so by extracting a @@ -433,7 +469,7 @@ be produced as follows: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path] ---- <1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. Uses the static `beneathPath` method on @@ -443,7 +479,7 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection] [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection] +include::{examples-dir}/com/example/restassured/Payload.java[tags=beneath-path] ---- <1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. Uses the static `beneathPath` method on @@ -457,6 +493,7 @@ example, the code above will result in a snippet named `response-fields-beneath-weather.temperature.adoc`. The identifier can be customized using the `withSubsectionId(String)` method: +[source,java,indent=0] ---- include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] ---- diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 9701f29f1..5802e2602 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -26,6 +26,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.snippet.Attributes.attributes; @@ -41,10 +42,19 @@ public void response() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .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"), // <2> + fieldWithPath("contact.name").description("The user's name")))); // <3> // end::response[] } + + public void subsection() throws Exception { + // tag::subsection[] + this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("index", responseFields( // <1> + subsectionWithPath("contact").description("The user's contact details")))); // <1> + // end::subsection[] + } public void explicitType() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) @@ -92,14 +102,14 @@ public void descriptorReuse() throws Exception { // end::book-array[] } - public void subsection() throws Exception { - // tag::subsection[] + public void subsectionBeneathPath() throws Exception { + // tag::beneath-path[] this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1> fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> fieldWithPath("low").description("The forecast low in degrees celcius")))); - // end::subsection[] + // end::beneath-path[] } } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index d54a7e6db..c5734d429 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -27,6 +27,7 @@ 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.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -39,12 +40,22 @@ public void response() throws Exception { // tag::response[] RestAssured.given(this.spec).accept("application/json") .filter(document("user", responseFields( // <1> - fieldWithPath("contact").description("The user's contact details"), // <2> + fieldWithPath("contact.name").description("The user's name"), // <2> fieldWithPath("contact.email").description("The user's email address")))) // <3> .when().get("/user/5") .then().assertThat().statusCode(is(200)); // end::response[] } + + public void subsection() throws Exception { + // tag::subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("user", responseFields( + subsectionWithPath("contact").description("The user's contact details")))) // <1> + .when().get("/user/5") + .then().assertThat().statusCode(is(200)); + // end::response[] + } public void explicitType() throws Exception { RestAssured.given(this.spec).accept("application/json") @@ -96,15 +107,15 @@ public void descriptorReuse() throws Exception { // end::book-array[] } - public void subsection() throws Exception { - // tag::subsection[] + public void subsectionBeneathPath() throws Exception { + // tag::beneath-path[] RestAssured.given(this.spec).accept("application/json") .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> fieldWithPath("low").description("The forecast low in degrees celcius")))) .when().get("/locations/1") .then().assertThat().statusCode(is(200)); - // end::subsection[] + // end::beneath-path[] } } diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index 2c014fc01..102426b96 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -26,6 +26,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.pr 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.payload.PayloadDocumentation.subsectionWithPath import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration @@ -75,8 +76,8 @@ class ApiDocumentationSpec extends Specification { fieldWithPath('appprofile').description('the profile of grails used in this project'), fieldWithPath('groovyversion').description('the version of groovy used in this project'), fieldWithPath('jvmversion').description('the version of the jvm used in this project'), - fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), - fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), + subsectionWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), + subsectionWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), ))) .when() .port(this.serverPort) @@ -123,14 +124,14 @@ class ApiDocumentationSpec extends Specification { requestFields( fieldWithPath('title').description('the title of the note'), fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') + subsectionWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') ), responseFields( fieldWithPath('class').description('the class of the resource'), fieldWithPath('id').description('the id of the note'), fieldWithPath('title').description('the title of the note'), fieldWithPath('body').description('the body of the note'), - fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'), + subsectionWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note') ))) .body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }') .when() diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index 82a30e34b..bfeeb6269 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -28,6 +28,7 @@ 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.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -59,7 +60,7 @@ @SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) @WebAppConfiguration public class ApiDocumentation { - + @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); @@ -116,7 +117,7 @@ public void indexExample() throws Exception { linkWithRel("tags").description("The <>"), linkWithRel("profile").description("The ALPS profile for the service")), responseFields( - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @@ -137,8 +138,8 @@ public void notesListExample() throws Exception { linkWithRel("self").description("Canonical link for this resource"), linkWithRel("profile").description("The ALPS profile for this resource")), responseFields( - fieldWithPath("_embedded.notes").description("An array of <>"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_embedded.notes").description("An array of <>"), + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -208,7 +209,7 @@ public void noteGetExample() throws Exception { responseFields( fieldWithPath("title").description("The title of the note"), fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -227,8 +228,8 @@ public void tagsListExample() throws Exception { linkWithRel("self").description("Canonical link for this resource"), linkWithRel("profile").description("The ALPS profile for this resource")), responseFields( - fieldWithPath("_embedded.tags").description("An array of <>"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_embedded.tags").description("An array of <>"), + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -310,7 +311,7 @@ public void tagGetExample() throws Exception { linkWithRel("notes").description("The <> that have this tag")), responseFields( fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @Test diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index f63458406..55b1e0368 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -33,6 +33,7 @@ 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.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -68,11 +69,11 @@ @SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) @WebAppConfiguration public class ApiDocumentation { - + @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - - private RestDocumentationResultHandler documentationHandler; + + private RestDocumentationResultHandler documentationHandler; @Autowired private NoteRepository noteRepository; @@ -93,13 +94,13 @@ public void setUp() { this.documentationHandler = document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())); - + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)) .alwaysDo(this.documentationHandler) .build(); } - + @Test public void headersExample() throws Exception { this.mockMvc @@ -140,7 +141,7 @@ public void indexExample() throws Exception { linkWithRel("notes").description("The <>"), linkWithRel("tags").description("The <>")), responseFields( - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -150,13 +151,13 @@ public void notesListExample() throws Exception { createNote("REST maturity model", "http://martinfowler.com/articles/richardsonMaturityModel.html"); createNote("Hypertext Application Language (HAL)", "http://stateless.co/hal_specification.html"); createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/"); - + this.mockMvc .perform(get("/notes")) .andExpect(status().isOk()) .andDo(this.documentationHandler.document( responseFields( - fieldWithPath("_embedded.notes").description("An array of <>")))); + subsectionWithPath("_embedded.notes").description("An array of <>")))); } @Test @@ -177,7 +178,7 @@ public void notesCreateExample() throws Exception { note.put("tags", Arrays.asList(tagLocation)); ConstrainedFields fields = new ConstrainedFields(NoteInput.class); - + this.mockMvc .perform(post("/notes") .contentType(MediaTypes.HAL_JSON) @@ -214,7 +215,7 @@ public void noteGetExample() throws Exception { .content(this.objectMapper.writeValueAsString(note))) .andExpect(status().isCreated()) .andReturn().getResponse().getHeader("Location"); - + this.mockMvc .perform(get(noteLocation)) .andExpect(status().isOk()) @@ -229,7 +230,7 @@ public void noteGetExample() throws Exception { responseFields( fieldWithPath("title").description("The title of the note"), fieldWithPath("body").description("The body of the note"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @@ -241,13 +242,13 @@ public void tagsListExample() throws Exception { createTag("REST"); createTag("Hypermedia"); createTag("HTTP"); - + this.mockMvc .perform(get("/tags")) .andExpect(status().isOk()) .andDo(this.documentationHandler.document( responseFields( - fieldWithPath("_embedded.tags").description("An array of <>")))); + subsectionWithPath("_embedded.tags").description("An array of <>")))); } @Test @@ -256,7 +257,7 @@ public void tagsCreateExample() throws Exception { tag.put("name", "REST"); ConstrainedFields fields = new ConstrainedFields(TagInput.class); - + this.mockMvc .perform(post("/tags") .contentType(MediaTypes.HAL_JSON) @@ -344,7 +345,7 @@ public void tagGetExample() throws Exception { linkWithRel("tagged-notes").description("The <> that have this tag")), responseFields( fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("<> to other resources")))); + subsectionWithPath("_links").description("<> to other resources")))); } @Test @@ -363,7 +364,7 @@ public void tagUpdateExample() throws Exception { tagUpdate.put("name", "RESTful"); ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class); - + this.mockMvc .perform(patch(tagLocation) .contentType(MediaTypes.HAL_JSON) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index a630cc385..8c83ab973 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -155,12 +155,10 @@ protected AbstractFieldsSnippet(String name, String type, for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { - Assert.notNull(descriptor.getDescription(), - "The descriptor for field '" + descriptor.getPath() - + "' must either have a description or" + " be marked as " - + "ignored"); + Assert.notNull(descriptor.getDescription() != null, + "The descriptor for '" + descriptor.getPath() + "' must have a" + + " description or it must be marked as ignored"); } - } this.fieldDescriptors = descriptors; this.ignoreUndocumentedFields = ignoreUndocumentedFields; diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index d68bb68d7..edc484433 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java index f1fff9ce4..1dc23a5ad 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonContentHandler.java @@ -65,7 +65,12 @@ public String getUndocumentedContent(List fieldDescriptors) { Object content = readContent(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath()); - this.fieldProcessor.remove(path, content); + if (describesSubsection(fieldDescriptor)) { + this.fieldProcessor.removeSubsection(path, content); + } + else { + this.fieldProcessor.remove(path, content); + } } if (!isEmpty(content)) { try { @@ -78,6 +83,10 @@ public String getUndocumentedContent(List fieldDescriptors) { return null; } + private boolean describesSubsection(FieldDescriptor fieldDescriptor) { + return fieldDescriptor instanceof SubsectionDescriptor; + } + private Object readContent() { try { return new ObjectMapper().readValue(this.rawContent, Object.class); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java index dbaf6d88c..dfbd5ff4a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/JsonFieldProcessor.java @@ -76,6 +76,17 @@ public void foundMatch(Match match) { }); } + void removeSubsection(final JsonFieldPath path, Object payload) { + traverse(new ProcessingContext(payload, path), new MatchCallback() { + + @Override + public void foundMatch(Match match) { + match.removeSubsection(); + } + + }); + } + private void traverse(ProcessingContext context, MatchCallback matchCallback) { final String segment = context.getSegment(); if (JsonFieldPath.isArraySegment(segment)) { @@ -149,12 +160,41 @@ public Object getValue() { @Override public void remove() { + Object removalCandidate = this.map.get(this.segment); + if (isMapWithEntries(removalCandidate) + || isListWithNonScalarEntries(removalCandidate)) { + return; + } this.map.remove(this.segment); if (this.map.isEmpty() && this.parent != null) { this.parent.remove(); } } + @Override + public void removeSubsection() { + this.map.remove(this.segment); + if (this.map.isEmpty() && this.parent != null) { + this.parent.removeSubsection(); + } + } + + private boolean isMapWithEntries(Object object) { + return object instanceof Map && !((Map) object).isEmpty(); + } + + private boolean isListWithNonScalarEntries(Object object) { + if (!(object instanceof List)) { + return false; + } + for (Object entry : (List) object) { + if (entry instanceof Map || entry instanceof List) { + return true; + } + } + return false; + } + } private static final class ListMatch implements Match { @@ -181,12 +221,35 @@ public Object getValue() { @Override public void remove() { + if (!itemIsEmpty()) { + return; + } this.items.remove(); if (this.list.isEmpty() && this.parent != null) { this.parent.remove(); } } + @Override + public void removeSubsection() { + this.items.remove(); + if (this.list.isEmpty() && this.parent != null) { + this.parent.removeSubsection(); + } + } + + private boolean itemIsEmpty() { + return !isMapWithEntries(this.item) && !isListWithEntries(this.item); + } + + private boolean isMapWithEntries(Object object) { + return object instanceof Map && !((Map) object).isEmpty(); + } + + private boolean isListWithEntries(Object object) { + return object instanceof List && !((List) object).isEmpty(); + } + } private interface MatchCallback { @@ -200,6 +263,8 @@ private interface Match { Object getValue(); void remove(); + + void removeSubsection(); } private static final class ProcessingContext { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 286cd185a..3f31ef76e 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -102,6 +102,74 @@ public static FieldDescriptor fieldWithPath(String path) { return new FieldDescriptor(path); } + /** + * Creates a {@code FieldDescriptor} that describes a subsection, i.e. a field and all + * of its descendants, with the given {@code path}. + *

          + * When documenting an XML payload, the {@code path} uses XPath, i.e. '/' is used to + * descend to a child node. + *

          + * When documenting a JSON payload, the {@code path} uses '.' to descend into a child + * object and ' {@code []}' to descend into an array. For example, with this JSON + * payload: + * + *

          +	 * {
          +	 *    "a":{
          +	 *        "b":[
          +	 *            {
          +	 *                "c":"one"
          +	 *            },
          +	 *            {
          +	 *                "c":"two"
          +	 *            },
          +	 *            {
          +	 *                "d":"three"
          +	 *            }
          +	 *        ]
          +	 *    }
          +	 * }
          +	 * 
          + * + * The following paths are all present: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
          PathValue
          {@code a}An object containing "b"
          {@code a.b}An array containing three objects
          {@code a.b[]}An array containing three objects
          {@code a.b[].c}An array containing the strings "one" and "two"
          {@code a.b[].d}The string "three"
          + *

          + * A subsection descriptor for the array with the path {@code a.b[]} will also + * describe its descendants {@code a.b[].c} and {@code a.b[].d}. + * + * @param path The path of the subsection + * @return a {@code SubsectionDescriptor} ready for further configuration + */ + public static SubsectionDescriptor subsectionWithPath(String path) { + return new SubsectionDescriptor(path); + } + /** * Returns a {@code Snippet} that will document the fields of the API operations's * request payload. The fields will be documented using the given {@code descriptors}. @@ -110,16 +178,19 @@ public static FieldDescriptor fieldWithPath(String path) { * descriptors, a failure will occur when the snippet 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. + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) + * @see FieldDescriptor#description(Object) */ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) { return requestFields(Arrays.asList(descriptors)); @@ -133,16 +204,18 @@ public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) * descriptors, a failure will occur when the snippet 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. + * a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet requestFields(List descriptors) { return new RequestFieldsSnippet(descriptors); @@ -158,6 +231,7 @@ public static RequestFieldsSnippet requestFields(List descripto * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( FieldDescriptor... descriptors) { @@ -174,6 +248,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( List descriptors) { @@ -187,19 +262,21 @@ public static RequestFieldsSnippet relaxedRequestFields( *

          * If a field is present in the request payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, 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. + * 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 with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet requestFields(Map attributes, FieldDescriptor... descriptors) { @@ -213,19 +290,21 @@ public static RequestFieldsSnippet requestFields(Map attributes, *

          * If a field is present in the request payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, 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. + * 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 with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param attributes the attributes * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet requestFields(Map attributes, List descriptors) { @@ -244,6 +323,7 @@ public static RequestFieldsSnippet requestFields(Map attributes, * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( Map attributes, FieldDescriptor... descriptors) { @@ -262,6 +342,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( Map attributes, List descriptors) { @@ -273,22 +354,25 @@ public static RequestFieldsSnippet relaxedRequestFields( * operations's request payload extracted by the given {@code subsectionExtractor}. * The fields will be documented using the given {@code descriptors}. *

          - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur.For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet requestFields( @@ -302,22 +386,25 @@ public static RequestFieldsSnippet requestFields( * API operations's request payload extracted by the given {@code subsectionExtractor} * . The fields will be documented using the given {@code descriptors}. *

          - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param descriptors the descriptions of the request payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet requestFields( @@ -339,6 +426,7 @@ public static RequestFieldsSnippet requestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( @@ -360,6 +448,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( @@ -374,16 +463,18 @@ public static RequestFieldsSnippet relaxedRequestFields( * The fields will be documented using the given {@code descriptors} and the given * {@code attributes} will be available during snippet generation. *

          - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param attributes the attributes @@ -391,6 +482,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet requestFields( @@ -405,16 +497,18 @@ public static RequestFieldsSnippet requestFields( * The fields will be documented using the given {@code descriptors} and the given * {@code attributes} will be available during snippet generation. *

          - * If a field is present in the request payload, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * payload, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param attributes the attributes @@ -422,6 +516,7 @@ public static RequestFieldsSnippet requestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet requestFields( @@ -445,6 +540,7 @@ public static RequestFieldsSnippet requestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( @@ -469,6 +565,7 @@ public static RequestFieldsSnippet relaxedRequestFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestFieldsSnippet relaxedRequestFields( @@ -483,22 +580,25 @@ public static RequestFieldsSnippet relaxedRequestFields( * {@code part} of the API operations's request payload. The fields will be documented * using the given {@code descriptors}. *

          - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, FieldDescriptor... descriptors) { @@ -510,21 +610,24 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * {@code part} of the API operations's request payload. The fields will be documented * using the given {@code descriptors}. *

          - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, List descriptors) { @@ -543,6 +646,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, FieldDescriptor... descriptors) { @@ -561,6 +665,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, List descriptors) { @@ -573,22 +678,25 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * using the given {@code descriptors} and the given {@code attributes} will be * available during snippet generation. *

          - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param attributes the attributes * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, FieldDescriptor... descriptors) { @@ -601,22 +709,25 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * using the given {@code descriptors} and the given {@code attributes} will be * available during snippet generation. *

          - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the payload of the request part, but is not documented by + * one of the descriptors, a failure will occur when the snippet is invoked. + * Similarly, if a field is documented, is not marked as optional, and is not present + * in the request part's payload, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param attributes the attributes * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, Map attributes, List descriptors) { @@ -637,6 +748,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, FieldDescriptor... descriptors) { @@ -657,6 +769,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @param descriptors the descriptions of the request part's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, Map attributes, List descriptors) { @@ -669,16 +782,18 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * be extracted by the given {@code subsectionExtractor}. The fields will be * documented using the given {@code descriptors}. *

          - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param subsectionExtractor the subsection extractor @@ -686,6 +801,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -700,16 +816,18 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * be extracted by the given {@code subsectionExtractor}. The fields will be * documented using the given {@code descriptors}. *

          - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param subsectionExtractor the subsection extractor @@ -717,6 +835,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -740,6 +859,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, @@ -764,6 +884,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, @@ -779,16 +900,18 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * documented using the given {@code descriptors} and the given {@code attributes} * will be available during snippet generation. *

          - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param subsectionExtractor the subsection extractor @@ -797,6 +920,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -813,16 +937,18 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * documented using the given {@code descriptors} and the given {@code attributes} * will be available during snippet generation. *

          - * If a field is present in the request part, but is not documented by one of the - * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the request - * part, 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. - *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If a field is present in the subsection of the request part payload, but is not + * documented by one of the descriptors, a failure will occur when the snippet is + * invoked. Similarly, if a field is documented, is not marked as optional, and is not + * present in the subsection, a failure will also occur. For payloads with a + * hierarchical structure, documenting a field with a + * {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its + * descendants are also treated as having been documented. + *

          + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param part the part name * @param subsectionExtractor the subsection extractor @@ -831,6 +957,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet requestPartFields(String part, @@ -857,6 +984,7 @@ public static RequestPartFieldsSnippet requestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, @@ -883,6 +1011,7 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, @@ -899,18 +1028,20 @@ public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, *

          * If a field is present in the response payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, 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. + * 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 with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) { return responseFields(Arrays.asList(descriptors)); @@ -923,19 +1054,21 @@ public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptor *

          * If a field is present in the response payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, 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. + * 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 with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -955,6 +1088,7 @@ public static ResponseFieldsSnippet responseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -973,6 +1107,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( List descriptors) { @@ -986,19 +1121,21 @@ public static ResponseFieldsSnippet relaxedResponseFields( *

          * If a field is present in the response payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, 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. + * 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 with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param attributes the attributes * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet responseFields(Map attributes, FieldDescriptor... descriptors) { @@ -1012,19 +1149,21 @@ public static ResponseFieldsSnippet responseFields(Map attribute *

          * If a field is present in the response payload, but is not documented by one of the * descriptors, a failure will occur when the snippet is invoked. Similarly, if a - * field is documented, is not marked as optional, and is not present in the response - * payload, 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. + * 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 with a {@link #subsectionWithPath(String) subsection descriptor} will mean + * that all of its descendants are also treated as having been documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param attributes the attributes * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet responseFields(Map attributes, List descriptors) { @@ -1043,6 +1182,7 @@ public static ResponseFieldsSnippet responseFields(Map attribute * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( Map attributes, FieldDescriptor... descriptors) { @@ -1061,6 +1201,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( Map attributes, List descriptors) { @@ -1077,18 +1218,21 @@ public static ResponseFieldsSnippet relaxedResponseFields( * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the response * payload, 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. + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -1107,18 +1251,21 @@ public static ResponseFieldsSnippet responseFields( * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the response * payload, 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. + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param descriptors the descriptions of the response payload's fields * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -1141,6 +1288,7 @@ public static ResponseFieldsSnippet responseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -1163,6 +1311,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -1182,12 +1331,14 @@ public static ResponseFieldsSnippet relaxedResponseFields( * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the response * payload, 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. + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param attributes the attributes @@ -1195,6 +1346,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -1215,12 +1367,14 @@ public static ResponseFieldsSnippet responseFields( * descriptors, a failure will occur when the snippet is invoked. Similarly, if a * field is documented, is not marked as optional, and is not present in the response * payload, 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. + * documenting a field with a {@link #subsectionWithPath(String) subsection + * descriptor} will mean that all of its descendants are also treated as having been + * documented. *

          - * If you do not want to document a field, a field descriptor can be marked as - * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the - * generated snippet while avoiding the failure described above. + * If you do not want to document a field or subsection, a descriptor can be + * {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or + * subsection will not appear in the generated snippet and the failure described above + * will not occur. * * @param subsectionExtractor the subsection extractor * @param attributes the attributes @@ -1228,6 +1382,7 @@ public static ResponseFieldsSnippet responseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet responseFields( @@ -1252,6 +1407,7 @@ public static ResponseFieldsSnippet responseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -1277,6 +1433,7 @@ public static ResponseFieldsSnippet relaxedResponseFields( * @return the snippet that will document the fields * @since 1.2.0 * @see #fieldWithPath(String) + * @see #subsectionWithPath(String) * @see #beneathPath(String) */ public static ResponseFieldsSnippet relaxedResponseFields( @@ -1298,11 +1455,13 @@ public static List applyPathPrefix(String pathPrefix, List descriptors) { List prefixedDescriptors = new ArrayList<>(); for (FieldDescriptor descriptor : descriptors) { - FieldDescriptor prefixedDescriptor = new FieldDescriptor( - pathPrefix + descriptor.getPath()) - .description(descriptor.getDescription()) - .type(descriptor.getType()) - .attributes(asArray(descriptor.getAttributes())); + String prefixedPath = pathPrefix + descriptor.getPath(); + FieldDescriptor prefixedDescriptor = descriptor instanceof SubsectionDescriptor + ? new SubsectionDescriptor(prefixedPath) + : new FieldDescriptor(prefixedPath); + prefixedDescriptor.description(descriptor.getDescription()) + .type(descriptor.getType()) + .attributes(asArray(descriptor.getAttributes())); if (descriptor.isIgnored()) { prefixedDescriptor.ignored(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java new file mode 100644 index 000000000..9d7756bfa --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/SubsectionDescriptor.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014-2016 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; + +/** + * A description of a subsection, i.e. a field and all of its descendants, in a request or + * response payload. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public class SubsectionDescriptor extends FieldDescriptor { + + /** + * Creates a new {@code SubsectionDescriptor} describing the subsection with the given + * {@code path}. + * @param path the path + */ + protected SubsectionDescriptor(String path) { + super(path); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java index 2e8f99e74..f5a466ff1 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/XmlContentHandler.java @@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import javax.xml.parsers.DocumentBuilder; @@ -36,6 +37,7 @@ import org.w3c.dom.Attr; import org.w3c.dom.Document; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; @@ -108,6 +110,7 @@ private XPathExpression createXPath(String fieldPath) @Override public String getUndocumentedContent(List fieldDescriptors) { Document payload = readPayload(); + List matchedButNotRemoved = new ArrayList<>(); for (FieldDescriptor fieldDescriptor : fieldDescriptors) { NodeList matchingNodes; try { @@ -124,17 +127,50 @@ public String getUndocumentedContent(List fieldDescriptors) { attr.getOwnerElement().removeAttributeNode(attr); } else { - node.getParentNode().removeChild(node); + if (fieldDescriptor instanceof SubsectionDescriptor + || isLeafNode(node)) { + node.getParentNode().removeChild(node); + } + else { + matchedButNotRemoved.add(node); + } } } } + removeLeafNodes(matchedButNotRemoved); if (payload.getChildNodes().getLength() > 0) { return prettyPrint(payload); } return null; } + private void removeLeafNodes(List candidates) { + boolean changed = true; + while (changed) { + changed = false; + Iterator iterator = candidates.iterator(); + while (iterator.hasNext()) { + Node node = iterator.next(); + if (isLeafNode(node)) { + node.getParentNode().removeChild(node); + iterator.remove(); + changed = true; + } + } + } + } + + private boolean isLeafNode(Node node) { + NodeList childNodes = node.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + if (childNodes.item(i) instanceof Element) { + return false; + } + } + return true; + } + private String prettyPrint(Document document) { try { StringWriter stringWriter = new StringWriter(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java index 395ca4dfb..145f61219 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/JsonFieldProcessorTests.java @@ -201,6 +201,26 @@ public void removeTopLevelMapEntry() { assertThat(payload.size(), equalTo(0)); } + @Test + public void mapWithEntriesIsNotRemovedWhenNotAlsoRemovingDescendants() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo"); + this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload); + assertThat(payload.size(), equalTo(1)); + } + + @Test + public void removeSubsectionRemovesMapWithEntries() { + Map payload = new HashMap<>(); + Map alpha = new HashMap<>(); + payload.put("a", alpha); + alpha.put("b", "bravo"); + this.fieldProcessor.removeSubsection(JsonFieldPath.compile("a"), payload); + assertThat(payload.size(), equalTo(0)); + } + @Test public void removeNestedMapEntry() { Map payload = new HashMap<>(); @@ -229,6 +249,51 @@ public void removeItemsInNestedArray() throws IOException { assertThat(payload.size(), equalTo(0)); } + @SuppressWarnings("unchecked") + @Test + public void removeDoesNotRemoveArrayWithMapEntries() throws IOException { + Map payload = new ObjectMapper() + .readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); + this.fieldProcessor.remove(JsonFieldPath.compile("a[]"), payload); + assertThat(payload.size(), equalTo(1)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeDoesNotRemoveArrayWithListEntries() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}", + Map.class); + this.fieldProcessor.remove(JsonFieldPath.compile("a[]"), payload); + assertThat(payload.size(), equalTo(1)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeRemovesArrayWithOnlyScalarEntries() throws IOException { + Map payload = new ObjectMapper() + .readValue("{\"a\": [\"bravo\", \"charlie\"]}", Map.class); + this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload); + assertThat(payload.size(), equalTo(0)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeSubsectionRemovesArrayWithMapEntries() throws IOException { + Map payload = new ObjectMapper() + .readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class); + this.fieldProcessor.removeSubsection(JsonFieldPath.compile("a[]"), payload); + assertThat(payload.size(), equalTo(0)); + } + + @SuppressWarnings("unchecked") + @Test + public void removeSubsectionRemovesArrayWithListEntries() throws IOException { + Map payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}", + Map.class); + this.fieldProcessor.removeSubsection(JsonFieldPath.compile("a[]"), payload); + assertThat(payload.size(), equalTo(0)); + } + @Test public void extractNestedEntryWithDotInKeys() throws IOException { Map payload = new HashMap<>(); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java index d6fd39986..80c9cdf14 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetFailureTests.java @@ -131,8 +131,8 @@ public void fieldWithExplicitSpecificTypeThatActuallyVaries() throws IOException @Test public void undocumentedXmlRequestField() throws IOException { this.thrown.expect(SnippetException.class); - this.thrown.expectMessage(startsWith( - "The following parts of the payload were not" + " documented:")); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); new RequestFieldsSnippet(Collections.emptyList()) .document(this.operationBuilder.request("http://localhost") .content("5").header(HttpHeaders.CONTENT_TYPE, @@ -140,6 +140,20 @@ public void undocumentedXmlRequestField() throws IOException { .build()); } + @Test + public void xmlDescendentsAreNotDocumentedByFieldDescriptor() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); + new RequestFieldsSnippet( + Arrays.asList(fieldWithPath("a").type("a").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .content("5") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); + } + @Test public void xmlRequestFieldWithNoType() throws IOException { this.thrown.expect(FieldTypeRequiredException.class); diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index d3ad4e25c..9ad807f74 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -36,6 +36,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; @@ -65,6 +66,19 @@ public void mapRequestWithFields() throws IOException { .build()); } + @Test + public void entireSubsectionsCanBeDocumented() throws IOException { + this.snippets.expectRequestFields() + .withContents(tableWithHeader("Path", "Type", "Description").row("`a`", + "`Object`", "one")); + + new RequestFieldsSnippet( + Arrays.asList(subsectionWithPath("a").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}") + .build()); + } + @Test public void subsectionOfMapRequest() throws IOException { this.snippets.expect("request-fields-beneath-a") @@ -121,6 +135,18 @@ public void ignoredRequestField() throws IOException { .content("{\"a\": 5, \"b\": 4}").build()); } + @Test + public void entireSubsectionCanBeIgnored() throws IOException { + this.snippets.expectRequestFields() + .withContents(tableWithHeader("Path", "Type", "Description").row("`c`", + "`Number`", "Field c")); + + new RequestFieldsSnippet(Arrays.asList(subsectionWithPath("a").ignored(), + fieldWithPath("c").description("Field c"))) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\": {\"b\": 5}, \"c\": 4}").build()); + } + @Test public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException { this.snippets.expectRequestFields() @@ -256,6 +282,20 @@ public void xmlRequestFields() throws IOException { .build()); } + @Test + public void entireSubsectionOfXmlPayloadCanBeDocumented() throws IOException { + this.snippets.expectRequestFields().withContents( + tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one")); + + new RequestFieldsSnippet( + Arrays.asList(subsectionWithPath("a").description("one").type("a"))) + .document(this.operationBuilder.request("http://localhost") + .content("5charlie") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_XML_VALUE) + .build()); + } + @Test public void additionalDescriptors() throws IOException { this.snippets.expectRequestFields() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java new file mode 100644 index 000000000..5c9f1b6ef --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/XmlContentHandlerTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2014-2016 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 java.util.Arrays; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; + +/** + * Tests for {@link XmlContentHandler}. + * + * @author Andy Wilkinson + */ +public class XmlContentHandlerTests { + + @Test + public void topLevelElementCanBeDocumented() { + String undocumentedContent = createHandler("5").getUndocumentedContent( + Arrays.asList(fieldWithPath("a").type("a").description("description"))); + assertThat(undocumentedContent, is(nullValue())); + } + + @Test + public void nestedElementCanBeDocumentedLeavingAncestors() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays.asList( + fieldWithPath("a/b").type("b").description("description"))); + assertThat(undocumentedContent, is(equalTo(String.format("%n")))); + } + + @Test + public void fieldDescriptorDoesNotDocumentEntireSubsection() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays + .asList(fieldWithPath("a").type("a").description("description"))); + assertThat(undocumentedContent, + is(equalTo(String.format("%n 5%n%n")))); + } + + @Test + public void subsectionDescriptorDocumentsEntireSubsection() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays.asList( + subsectionWithPath("a").type("a").description("description"))); + assertThat(undocumentedContent, is(nullValue())); + } + + @Test + public void multipleElementsCanBeInDescendingOrderDocumented() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays.asList( + fieldWithPath("a").type("a").description("description"), + fieldWithPath("a/b").type("b").description("description"))); + assertThat(undocumentedContent, is(nullValue())); + } + + @Test + public void multipleElementsCanBeInAscendingOrderDocumented() { + String undocumentedContent = createHandler("5") + .getUndocumentedContent(Arrays.asList( + fieldWithPath("a/b").type("b").description("description"), + fieldWithPath("a").type("a").description("description"))); + assertThat(undocumentedContent, is(nullValue())); + } + + private XmlContentHandler createHandler(String xml) { + return new XmlContentHandler(xml.getBytes()); + } + +} diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 5429591fc..26ea2c958 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -72,6 +72,7 @@ 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.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; @@ -316,9 +317,10 @@ public void responseFieldsSnippet() throws Exception { mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links", responseFields( - fieldWithPath("a").description("The description"), - fieldWithPath("links").description("Links to other resources")))); + .andDo(document("links", + responseFields(fieldWithPath("a").description("The description"), + subsectionWithPath("links") + .description("Links to other resources")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), "http-request.adoc", "http-response.adoc", "curl-request.adoc", diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index d1089ecc7..6d1a84e37 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -65,6 +65,7 @@ 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.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; @@ -204,7 +205,7 @@ public void responseFieldsSnippet() throws Exception { given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) .filter(document("response-fields", responseFields(fieldWithPath("a").description("The description"), - fieldWithPath("links") + subsectionWithPath("links") .description("Links to other resources")))) .accept("application/json").get("/").then().statusCode(200); From 7ff45d273ec5b92a0c280d08db01637edb37cb3d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 28 Oct 2016 16:19:19 +0100 Subject: [PATCH 196/898] Call the right endpoint in REST Assured field reuse example Closes gh-324 --- docs/src/test/java/com/example/restassured/Payload.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index 155e357e5..75022a487 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -90,7 +90,7 @@ public void descriptorReuse() throws Exception { .filter(document("books", responseFields( fieldWithPath("[]").description("An array of books")) // <1> .andWithPrefix("[].", book))) // <2> - .when().get("/books/1") + .when().get("/books") .then().assertThat().statusCode(is(200)); // end::book-array[] } From 1393182182cd270cffe153be2bf9e69a4b917d44 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 28 Oct 2016 21:25:30 +0100 Subject: [PATCH 197/898] Upgrade Spring Boot-based samples and integration tests to use 1.4 Closes gh-327 --- samples/rest-assured/build.gradle | 5 ++--- .../SampleRestAssuredApplicationTests.java | 17 ++++++++--------- samples/rest-notes-slate/build.gradle | 2 +- .../com/example/notes/ApiDocumentation.java | 13 ++++++------- samples/rest-notes-spring-data-rest/pom.xml | 3 +-- .../com/example/notes/ApiDocumentation.java | 10 ++++------ .../notes/GettingStartedDocumentation.java | 14 ++++++-------- .../rest-notes-spring-hateoas/build.gradle | 3 +-- .../com/example/notes/ApiDocumentation.java | 10 ++++------ .../notes/GettingStartedDocumentation.java | 14 ++++++-------- samples/testng/build.gradle | 7 +++---- .../testng/SampleTestNgApplicationTests.java | 6 ++---- spring-restdocs-restassured/build.gradle | 19 +++---------------- .../RestAssuredRequestConverterTests.java | 18 +++++++----------- ...uredRestDocumentationIntegrationTests.java | 14 +++++--------- 15 files changed, 59 insertions(+), 96 deletions(-) diff --git a/samples/rest-assured/build.gradle b/samples/rest-assured/build.gradle index b8776d376..a55dd6b13 100644 --- a/samples/rest-assured/build.gradle +++ b/samples/rest-assured/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.1.RELEASE' } } @@ -31,12 +31,11 @@ ext { } ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' -ext['spring.version']='4.3.1.RELEASE' dependencies { compile 'org.springframework.boot:spring-boot-starter-web' testCompile 'org.springframework.boot:spring-boot-starter-test' - testCompile "org.springframework.restdocs:spring-restdocs-restassured:${project.ext['spring-restdocs.version']}" + testCompile 'org.springframework.restdocs:spring-restdocs-restassured' asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:${project.ext['spring-restdocs.version']}" } diff --git a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java index 6eea9876d..c1c4de794 100644 --- a/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java +++ b/samples/rest-assured/src/test/java/com/example/restassured/SampleRestAssuredApplicationTests.java @@ -27,26 +27,25 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.boot.test.WebIntegrationTest; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; -@SpringApplicationConfiguration(classes=SampleRestAssuredApplication.class) -@WebIntegrationTest("server.port=0") +@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT) @RunWith(SpringJUnit4ClassRunner.class) public class SampleRestAssuredApplicationTests { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); - + private RequestSpecification documentationSpec; - - @Value("${local.server.port}") + + @LocalServerPort private int port; @Before @@ -59,7 +58,7 @@ public void setUp() { public void sample() throws Exception { given(this.documentationSpec) .accept("text/plain") - .filter(document("sample", + .filter(document("sample", preprocessRequest(modifyUris() .scheme("https") .host("api.example.com") diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index 157e71255..d65d9d483 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.1.RELEASE' } } diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java index 61564aca9..073af2bcd 100644 --- a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -28,10 +28,11 @@ 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.templates.TemplateFormats.markdown; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.springframework.restdocs.templates.TemplateFormats.markdown; + import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -43,21 +44,19 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.databind.ObjectMapper; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSlate.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class ApiDocumentation { @Rule diff --git a/samples/rest-notes-spring-data-rest/pom.xml b/samples/rest-notes-spring-data-rest/pom.xml index 022c19226..3e6285ba0 100644 --- a/samples/rest-notes-spring-data-rest/pom.xml +++ b/samples/rest-notes-spring-data-rest/pom.xml @@ -11,14 +11,13 @@ org.springframework.boot spring-boot-starter-parent - 1.3.6.RELEASE + 1.4.1.RELEASE UTF-8 1.7 - 4.3.1.RELEASE 1.2.0.BUILD-SNAPSHOT diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index bfeeb6269..fe4367311 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -44,21 +44,19 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import com.fasterxml.jackson.databind.ObjectMapper; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class ApiDocumentation { @Rule diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java index 407a8119c..543781b72 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -38,11 +38,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -52,11 +51,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class GettingStartedDocumentation { - + @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); diff --git a/samples/rest-notes-spring-hateoas/build.gradle b/samples/rest-notes-spring-hateoas/build.gradle index a44f3f942..0306858fe 100644 --- a/samples/rest-notes-spring-hateoas/build.gradle +++ b/samples/rest-notes-spring-hateoas/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.1.RELEASE' } } @@ -30,7 +30,6 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index 55b1e0368..8b7907e72 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -49,15 +49,14 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.constraints.ConstraintDescriptions; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.StringUtils; @@ -65,9 +64,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class ApiDocumentation { @Rule diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java index e147deeb4..101fc0d36 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/GettingStartedDocumentation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -41,11 +41,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.hateoas.MediaTypes; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -55,11 +54,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.JsonPath; -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class) -@WebAppConfiguration +@RunWith(SpringRunner.class) +@SpringBootTest public class GettingStartedDocumentation { - + @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation(); diff --git a/samples/testng/build.gradle b/samples/testng/build.gradle index 3be74243e..3b9abc857 100644 --- a/samples/testng/build.gradle +++ b/samples/testng/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.3.6.RELEASE' + classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.4.1.RELEASE' } } @@ -30,7 +30,6 @@ ext { snippetsDir = file('build/generated-snippets') } -ext['spring.version']='4.3.1.RELEASE' ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' dependencies { @@ -38,11 +37,11 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-web' - testCompile('org.springframework:spring-test') { + testCompile('org.springframework.boot:spring-boot-starter-test') { exclude group: 'junit', module: 'junit;' } - testCompile 'org.testng:testng:6.9.10' testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' + testCompile 'org.testng:testng:6.9.10' } test { diff --git a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java index 9c4a9c9b6..3a0e58e48 100644 --- a/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java +++ b/samples/testng/src/test/java/com/example/testng/SampleTestNgApplicationTests.java @@ -24,10 +24,9 @@ import java.lang.reflect.Method; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.restdocs.ManualRestDocumentation; import org.springframework.test.context.testng.AbstractTestNGSpringContextTests; -import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @@ -35,8 +34,7 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; -@SpringApplicationConfiguration(classes=SampleTestNgApplication.class) -@WebAppConfiguration +@SpringBootTest public class SampleTestNgApplicationTests extends AbstractTestNGSpringContextTests { private final ManualRestDocumentation restDocumentation = new ManualRestDocumentation(); diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index a861c7e51..a5cb2d1d5 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -1,10 +1,5 @@ description = 'Spring REST Docs REST Assured' -boolean isPlatformUsingBootOneFour() { - def bootVersion = dependencyManagement.springIoTestRuntime.managedVersions['org.springframework.boot:spring-boot'] - bootVersion.startsWith('1.4') -} - dependencies { compile project(':spring-restdocs-core') compile 'com.jayway.restassured:rest-assured' @@ -12,20 +7,12 @@ dependencies { testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.6.RELEASE' - testCompile 'org.springframework:spring-test' + testCompile 'org.springframework.boot:spring-boot-starter-web:1.4.1.RELEASE' + testCompile 'org.springframework.boot:spring-boot-test:1.4.1.RELEASE' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') testRuntime 'commons-logging:commons-logging:1.2' } test { jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" -} - -afterEvaluate { - if (project.hasProperty('platformVersion') && platformUsingBootOneFour) { - dependencies { - springIoTestRuntime 'org.springframework.boot:spring-boot-test' - } - } -} +} \ No newline at end of file diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 9c4f7615e..562a82603 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -33,18 +33,16 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.IntegrationTest; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.restassured.RestAssuredRequestConverterTests.TestApplication; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -57,10 +55,8 @@ * * @author Andy Wilkinson */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = TestApplication.class) -@WebAppConfiguration -@IntegrationTest("server.port=0") +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class RestAssuredRequestConverterTests { @Rule @@ -68,7 +64,7 @@ public class RestAssuredRequestConverterTests { private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); - @Value("${local.server.port}") + @LocalServerPort private int port; @Test diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 6d1a84e37..da43824bc 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -32,17 +32,15 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.IntegrationTest; -import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -85,10 +83,8 @@ * * @author Andy Wilkinson */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = TestApplication.class) -@WebAppConfiguration -@IntegrationTest("server.port=0") +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class RestAssuredRestDocumentationIntegrationTests { @Rule From dec3727da1c6a333bb17db4cfc8f15bb7d3bf99d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 31 Oct 2016 12:46:36 +0000 Subject: [PATCH 198/898] Add support for documenting body of a request, response or request part Closes gh-318 Closes gh-319 --- .../docs/asciidoc/documenting-your-api.adoc | 178 ++++++++++++++---- docs/src/docs/asciidoc/getting-started.adoc | 4 +- docs/src/test/java/com/example/Payload.java | 6 +- .../java/com/example/mockmvc/Payload.java | 93 +++++---- .../example/mockmvc/RequestPartPayload.java | 21 ++- .../java/com/example/restassured/Payload.java | 18 +- .../restassured/RequestPartPayload.java | 19 +- .../restdocs/config/SnippetConfigurer.java | 4 +- .../restdocs/payload/AbstractBodySnippet.java | 125 ++++++++++++ .../payload/PayloadDocumentation.java | 158 ++++++++++++++++ .../restdocs/payload/RequestBodySnippet.java | 84 +++++++++ .../payload/RequestPartBodySnippet.java | 111 +++++++++++ .../restdocs/payload/ResponseBodySnippet.java | 84 +++++++++ .../asciidoctor/default-request-body.snippet | 4 + .../default-request-part-body.snippet | 4 + .../asciidoctor/default-response-body.snippet | 4 + .../markdown/default-request-body.snippet | 3 + .../default-request-part-body.snippet | 3 + .../markdown/default-response-body.snippet | 3 + .../restdocs/AbstractSnippetTests.java | 6 +- .../RestDocumentationConfigurerTests.java | 10 +- .../payload/RequestBodyPartSnippetTests.java | 95 ++++++++++ .../payload/RequestBodySnippetTests.java | 92 +++++++++ .../payload/ResponseBodySnippetTests.java | 92 +++++++++ .../restdocs/test/SnippetMatchers.java | 13 +- .../request-body-with-language.snippet | 4 + .../request-part-body-with-language.snippet | 4 + .../response-body-with-language.snippet | 4 + .../request-body-with-language.snippet | 3 + .../request-part-body-with-language.snippet | 3 + .../response-body-with-language.snippet | 3 + 31 files changed, 1160 insertions(+), 95 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet create mode 100644 spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/request-part-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/asciidoctor/response-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/request-part-body-with-language.snippet create mode 100644 spring-restdocs-core/src/test/resources/custom-snippet-templates/markdown/response-body-with-language.snippet diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index a1eeb908f..e6068ce7f 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -105,12 +105,28 @@ descriptors to a snippet that's preconfigured to ignore certain links. For examp include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links] ---- + + [[documenting-your-api-request-response-payloads]] === Request and response payloads In addition to the hypermedia-specific support <>, support for general documentation of request and response payloads is also -provided. Consider the following payload: +provided. + +By default, Spring REST Docs will automatically generate snippets for the body of the +request and the body of the response. These snippets are named `request-body.adoc` and +`response-body.adoc` respectively. + + + +[[documenting-your-api-request-response-payloads-fields]] +==== Request and response fields + +To provide more detailed documentation of a request or response payload, support for +documenting the payload's fields is provided. + +Consider the following payload: [source,json,indent=0] ---- @@ -122,7 +138,7 @@ provided. Consider the following payload: } ---- -It can be documented like this: +Its fields can be documented like this: [source,java,indent=0,role="primary"] .MockMvc @@ -195,11 +211,11 @@ TIP: By default, Spring REST Docs will assume that the payload you are documenti JSON. If you want to document an XML payload the content type of the request or response must be compatible with `application/xml`. -[[documenting-your-api-request-response-payloads-json]] -==== JSON payloads +[[documenting-your-api-request-response-payloads-fields-json]] +===== Fields in JSON payloads -[[documenting-your-api-request-response-payloads-json-field-paths]] -===== JSON field paths +[[documenting-your-api-request-response-payloads-fields-json-field-paths]] +====== JSON field paths JSON field paths use either dot notation or bracket notation. Dot notation uses '.' to separate each key in the path; `a.b`, for example. Bracket notation wraps each key in @@ -288,8 +304,8 @@ found in the following array: -[[documenting-your-api-request-response-payloads-json-field-types]] -===== JSON field types +[[documenting-your-api-request-response-payloads-fields-json-field-types]] +====== JSON field types When a field is documented, Spring REST Docs will attempt to determine its type by examining the payload. Seven different types are supported: @@ -340,18 +356,18 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=explicit-type] <1> Set the field's type to `string`. -[[documenting-your-api-request-response-payloads-xml]] -==== XML payloads +[[documenting-your-api-request-response-payloads-fields-xml]] +===== XML payloads -[[documenting-your-api-request-response-payloads-xml-field-paths]] -===== XML field paths +[[documenting-your-api-request-response-payloads-fields-xml-field-paths]] +====== XML field paths XML field paths are described using XPath. `/` is used to descend into a child node. -[[documenting-your-api-request-response-payloads-xml-field-types]] -===== XML field types +[[documenting-your-api-request-response-payloads-fields-xml-field-types]] +====== XML field types When documenting an XML payload, you must provide a type for the field using the `type(Object)` method on `FieldDescriptor`. The result of the supplied type's `toString` @@ -359,8 +375,8 @@ method will be used in the documentation. -[[documenting-your-api-request-response-payloads-reusing-field-descriptors]] -==== Reusing field descriptors +[[documenting-your-api-request-response-payloads-fields-reusing-field-descriptors]] +===== Reusing field descriptors In addition to the general support for <>, the request and response snippets allow additional descriptors to be @@ -445,7 +461,10 @@ If a payload is large or structurally complex, it can be useful to document individual sections of the payload. REST Docs allows you to do so by extracting a subsection of the payload and then documenting it. -Consider the following JSON response payload: +[[documenting-your-api-request-response-payloads-subsections-body]] +===== Documenting a subsection of a request or response body + +Consider the following JSON response body: [source,json,indent=0] ---- @@ -463,13 +482,64 @@ Consider the following JSON response payload: } ---- -A snippet that documents the fields of the `temperature` object (`high` and `low`) can -be produced as follows: +A snippet that documents the `temperature` object can be produces as follows: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path] +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. Uses the static + `responseBody` and `beneathPath` methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet + for the request body, `requestBody` can be used in place of `responseBody`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=body-subsection] +---- +<1> Produce a snippet containing a subsection of the response body. Uses the static + `responseBody` and `beneathPath` methods on + `org.springframework.restdocs.payload.PayloadDocumentation`. To produce a snippet + for the request body, `requestBody` can be used in place of `responseBody`. + +The result is a snippet with the following contents: + +[source,json,indent=0] +---- + { + "temperature": { + "high": 21.2, + "low": 14.8 + } + } +---- + +To make the snippet's name distinct, an identifier for the subsection is included. By +default, this identifier is `beneath-${path}`. For example, the code above will result in +a snippet named `response-body-beneath-weather.temperature.adoc`. The identifier can +be customized using the `withSubsectionId(String)` method: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] +---- + +This example will result in a snippet named `request-body-temp.adoc`. + + + +[[documenting-your-api-request-response-payloads-subsections-fields]] +===== Documenting the fields of a subsection of a request or response + +As well as documenting a subsection of a request or response body, it's also possible to +document the fields in a particular subsection. A snippet that documents the fields of the `temperature` object (`high` and `low`) can be produced as follows: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=fields-subsection] ---- <1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. Uses the static `beneathPath` method on @@ -479,7 +549,7 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path] [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/Payload.java[tags=beneath-path] +include::{examples-dir}/com/example/restassured/Payload.java[tags=fields-subsection] ---- <1> Produce a snippet describing the fields in the subsection of the response payload beneath the path `weather.temperature`. Uses the static `beneathPath` method on @@ -490,15 +560,7 @@ The result is a snippet that contains a table describing the `high` and `low` fi of `weather.temperature`. To make the snippet's name distinct, an identifier for the subsection is included. By default, this identifier is `beneath-${path}`. For example, the code above will result in a snippet named -`response-fields-beneath-weather.temperature.adoc`. The identifier can be customized using -the `withSubsectionId(String)` method: - -[source,java,indent=0] ----- -include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id] ----- - -This example will result in a snippet named `response-fields-temp.adoc`. +`response-fields-beneath-weather.temperature.adoc`. @@ -679,12 +741,51 @@ above. === Request part payloads The payload of a request part can be documented in much the same way as the -<>: +<> with support +for documenting a request part's body and its fields. + + + +[[documenting-your-api-request-parts-payloads-fields]] +==== Documenting a request part's body + +A snippet containing the body of a request part can be generated: [source,java,indent=0,role="primary"] .MockMvc ---- -include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=payload] +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the + of the request part named `metadata`. Uses the static `requestPartBody` method on + `PayloadDocumentation`. + payload. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=body] +---- +<1> Configure Spring REST docs to produce a snippet containing the body of the request + part named `metadata`. Uses the static `requestPartBody` method on + `PayloadDocumentation`. + +The result is a snippet `request-part-${part-name}-body.adoc` that contains the part's +body. For example, documenting a part named `metadata` will produce a snippet named +`request-part-metadata-body.adoc`. + + + +[[documenting-your-api-request-parts-payloads-fields]] +==== Documenting a request part's fields + +A request part's fields can be documented in much the same way as the fields of a request +or response: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=fields] ---- <1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. Uses the static `requestPartFields` method on @@ -696,7 +797,7 @@ include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=payload [source,java,indent=0,role="secondary"] .REST Assured ---- -include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=payload] +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=fields] ---- <1> Configure Spring REST docs to produce a snippet describing the fields in the payload of the request part named `metadata`. Uses the static `requestPartFields` method on @@ -930,6 +1031,13 @@ documented | `http-response.adoc` | Contains the HTTP response that was returned + +| `request-body.adoc` +| Contains the body of the request that was sent + +| `response-body.adoc` +| Contains the body of the response that was returned + |=== You can configure which snippets are produced by default. Please refer to the @@ -1076,6 +1184,4 @@ and adds a title: ---- <1> Add a title to the table <2> Add a new column named "Constraints" -<3> Include the descriptors' `constraints` attribute in each row of the table - - +<3> Include the descriptors' `constraints` attribute in each row of the table \ No newline at end of file diff --git a/docs/src/docs/asciidoc/getting-started.adoc b/docs/src/docs/asciidoc/getting-started.adoc index d70a78ce7..f84f0f660 100644 --- a/docs/src/docs/asciidoc/getting-started.adoc +++ b/docs/src/docs/asciidoc/getting-started.adoc @@ -406,12 +406,14 @@ static `document` method on <4> Invoke the root (`/`) of the service. <5> Assert that the service produce the expected response. -By default, four snippets are written: +By default, six snippets are written: * `/index/curl-request.adoc` * `/index/http-request.adoc` * `/index/http-response.adoc` * `/index/httpie-request.adoc` + * `/index/request-body.adoc` + * `/index/response-body.adoc` Refer to <> for more information about these and other snippets that can be produced by Spring REST Docs. diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index f553927df..eb98f69fa 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -20,7 +20,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; public class Payload { @@ -35,9 +35,7 @@ public void bookFieldDescriptors() { public void customSubsectionId() { // tag::custom-subsection-id[] - responseFields(beneathPath("weather.temperature").withSubsectionId("temp"), - fieldWithPath("high").description("…"), - fieldWithPath("low").description("…")); + responseBody(beneathPath("weather.temperature").withSubsectionId("temp")); // end::custom-subsection-id[] } diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 5802e2602..d1b12200e 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -26,9 +26,10 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.snippet.Attributes.attributes; import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -40,46 +41,50 @@ public class Payload { public void response() throws Exception { // tag::response[] this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("index", responseFields( // <1> - fieldWithPath("contact.email").description("The user's email address"), // <2> - fieldWithPath("contact.name").description("The user's name")))); // <3> + .andExpect(status().isOk()) + .andDo(document("index", + responseFields( // <1> + fieldWithPath("contact.email") + .description("The user's email address"), // <2> + fieldWithPath("contact.name").description("The user's name")))); // <3> // end::response[] } - + public void subsection() throws Exception { // tag::subsection[] this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("index", responseFields( // <1> - subsectionWithPath("contact").description("The user's contact details")))); // <1> + .andExpect(status().isOk()) + .andDo(document("index", + responseFields( // <1> + subsectionWithPath("contact") + .description("The user's contact details")))); // <1> // end::subsection[] } public void explicitType() throws Exception { this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - // tag::explicit-type[] - .andDo(document("index", responseFields( - fieldWithPath("contact.email") - .type(JsonFieldType.STRING) // <1> - .description("The user's email address")))); - // end::explicit-type[] + .andExpect(status().isOk()) + // tag::explicit-type[] + .andDo(document("index", + responseFields( + fieldWithPath("contact.email").type(JsonFieldType.STRING) // <1> + .description("The user's email address")))); + // end::explicit-type[] } public void constraints() throws Exception { this.mockMvc.perform(post("/users/").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - // tag::constraints[] - .andDo(document("create-user", requestFields( - attributes(key("title").value("Fields for user creation")), // <1> - fieldWithPath("name").description("The user's name") - .attributes(key("constraints") - .value("Must not be null. Must not be empty")), // <2> - fieldWithPath("email").description("The user's email address") - .attributes(key("constraints") - .value("Must be a valid email address"))))); // <3> - // end::constraints[] + .andExpect(status().isOk()) + // tag::constraints[] + .andDo(document("create-user", requestFields( + attributes(key("title").value("Fields for user creation")), // <1> + fieldWithPath("name").description("The user's name") + .attributes(key("constraints") + .value("Must not be null. Must not be empty")), // <2> + fieldWithPath("email").description("The user's email address") + .attributes(key("constraints") + .value("Must be a valid email address"))))); // <3> + // end::constraints[] } public void descriptorReuse() throws Exception { @@ -89,27 +94,39 @@ public void descriptorReuse() throws Exception { // tag::single-book[] this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(document("book", responseFields(book))); // <1> + .andExpect(status().isOk()).andDo(document("book", responseFields(book))); // <1> // end::single-book[] // tag::book-array[] this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("book", responseFields( - fieldWithPath("[]").description("An array of books")) // <1> - .andWithPrefix("[].", book))); // <2> + .andDo(document("book", + responseFields( + fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))); // <2> // end::book-array[] } - public void subsectionBeneathPath() throws Exception { - // tag::beneath-path[] + public void fieldsSubsection() throws Exception { + // tag::fields-subsection[] this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1> - fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> - fieldWithPath("low").description("The forecast low in degrees celcius")))); - // end::beneath-path[] + .andDo(document("location", + responseFields(beneathPath("weather.temperature"), // <1> + fieldWithPath("high").description( + "The forecast high in degrees celcius"), // <2> + fieldWithPath("low") + .description("The forecast low in degrees celcius")))); + // end::fields-subsection[] + } + + public void bodySubsection() throws Exception { + // tag::body-subsection[] + this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andDo(document("location", + responseBody(beneathPath("weather.temperature")))); // <1> + + // end::body-subsection[] } } diff --git a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java index 56d6ae85d..e020a4e73 100644 --- a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java +++ b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java @@ -23,6 +23,7 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.fileUpload; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -30,8 +31,8 @@ public class RequestPartPayload { private MockMvc mockMvc; - public void response() throws Exception { - // tag::payload[] + public void fields() throws Exception { + // tag::fields[] MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", "<>".getBytes()); MockMultipartFile metadata = new MockMultipartFile("metadata", "", @@ -42,7 +43,21 @@ public void response() throws Exception { .andExpect(status().isOk()) .andDo(document("image-upload", requestPartFields("metadata", // <1> fieldWithPath("version").description("The version of the image")))); // <2> - // end::payload[] + // end::fields[] + } + + public void body() throws Exception { + // tag::body[] + MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", + "<>".getBytes()); + MockMultipartFile metadata = new MockMultipartFile("metadata", "", + "application/json", "{ \"version\": \"1.0\"}".getBytes()); + + this.mockMvc.perform(fileUpload("/images").file(image).file(metadata) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("image-upload", requestPartBody("metadata"))); // <1> + // end::body[] } } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index f7789cc50..b538acbb0 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -26,6 +26,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; @@ -46,7 +47,7 @@ public void response() throws Exception { .then().assertThat().statusCode(is(200)); // end::response[] } - + public void subsection() throws Exception { // tag::subsection[] RestAssured.given(this.spec).accept("application/json") @@ -107,15 +108,24 @@ public void descriptorReuse() throws Exception { // end::book-array[] } - public void subsectionBeneathPath() throws Exception { - // tag::beneath-path[] + public void fieldsSubsection() throws Exception { + // tag::fields-subsection[] RestAssured.given(this.spec).accept("application/json") .filter(document("location", responseFields(beneathPath("weather.temperature"), // <1> fieldWithPath("high").description("The forecast high in degrees celcius"), // <2> fieldWithPath("low").description("The forecast low in degrees celcius")))) .when().get("/locations/1") .then().assertThat().statusCode(is(200)); - // end::beneath-path[] + // end::fields-subsection[] + } + + public void bodySubsection() throws Exception { + // tag::body-subsection[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("location", responseBody(beneathPath("weather.temperature")))) // <1> + .when().get("/locations/1") + .then().assertThat().statusCode(is(200)); + // end::body-subsection[] } } diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java index 5dc8eda54..ec83908ef 100644 --- a/docs/src/test/java/com/example/restassured/RequestPartPayload.java +++ b/docs/src/test/java/com/example/restassured/RequestPartPayload.java @@ -25,6 +25,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; @@ -32,8 +33,8 @@ public class RequestPartPayload { private RequestSpecification spec; - public void response() throws Exception { - // tag::payload[] + public void fields() throws Exception { + // tag::fields[] Map metadata = new HashMap<>(); metadata.put("version", "1.0"); RestAssured.given(this.spec).accept("application/json") @@ -42,7 +43,19 @@ public void response() throws Exception { .when().multiPart("image", new File("image.png"), "image/png") .multiPart("metadata", metadata).post("images") .then().assertThat().statusCode(is(200)); - // end::payload[] + // end::fields[] + } + + public void body() throws Exception { + // tag::body[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartBody("metadata"))) // <1> + .when().multiPart("image", new File("image.png"), "image/png") + .multiPart("metadata", metadata).post("images") + .then().assertThat().statusCode(is(200)); + // end::body[] } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java index 0da0b48c9..9e96055cf 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/config/SnippetConfigurer.java @@ -25,6 +25,7 @@ import org.springframework.restdocs.cli.CliDocumentation; import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpDocumentation; +import org.springframework.restdocs.payload.PayloadDocumentation; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.templates.TemplateFormat; import org.springframework.restdocs.templates.TemplateFormats; @@ -42,7 +43,8 @@ public abstract class SnippetConfigurer private List defaultSnippets = new ArrayList<>(Arrays.asList( CliDocumentation.curlRequest(), CliDocumentation.httpieRequest(), - HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse())); + HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse(), + PayloadDocumentation.requestBody(), PayloadDocumentation.responseBody())); /** * The default encoding for documentation snippets. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java new file mode 100644 index 000000000..0010ea28e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractBodySnippet.java @@ -0,0 +1,125 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.ModelCreationException; +import org.springframework.restdocs.snippet.TemplatedSnippet; + +/** + * Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that + * document a RESTful resource's request or response body. + * + * @author Andy Wilkinson + */ +public abstract class AbstractBodySnippet extends TemplatedSnippet { + + private final PayloadSubsectionExtractor subsectionExtractor; + + /** + * Creates a new {@code AbstractBodySnippet} that will produce a snippet named + * {@code -body} using a template named {@code -body}. The snippet will + * contain the subsection of the body extracted by the given + * {@code subsectionExtractor}. The given {@code attributes} will be included in the + * model during template rendering + * + * @param type the type of the body + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + */ + protected AbstractBodySnippet(String type, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + this(type, type, subsectionExtractor, attributes); + } + + /** + * Creates a new {@code AbstractBodySnippet} that will produce a snippet named + * {@code -body} using a template named {@code -body}. The snippet will + * contain the subsection of the body extracted by the given + * {@code subsectionExtractor}. The given {@code attributes} will be included in the + * model during template rendering + * + * @param name the name of the snippet + * @param type the type of the body + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + */ + protected AbstractBodySnippet(String name, String type, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super(name + "-body" + + (subsectionExtractor != null + ? "-" + subsectionExtractor.getSubsectionId() : ""), + type + "-body", attributes); + this.subsectionExtractor = subsectionExtractor; + } + + @Override + protected Map createModel(Operation operation) { + try { + MediaType contentType = getContentType(operation); + byte[] content = getContent(operation); + if (this.subsectionExtractor != null) { + content = this.subsectionExtractor.extractSubsection(content, + contentType); + } + Charset charset = extractCharset(contentType); + String body = charset != null ? new String(content, charset) + : new String(content); + Map model = new HashMap<>(); + model.put("body", body); + return model; + } + catch (IOException ex) { + throw new ModelCreationException(ex); + } + } + + private Charset extractCharset(MediaType contentType) { + if (contentType == null) { + return null; + } + return contentType.getCharset(); + } + + /** + * Returns the content of the request or response extracted from the given + * {@code operation}. + * + * @param operation The operation + * @return The content + * @throws IOException if the content cannot be extracted + */ + protected abstract byte[] getContent(Operation operation) throws IOException; + + /** + * Returns the content type of the request or response extracted from the given + * {@code operation}. + * + * @param operation The operation + * @return The content type + */ + protected abstract MediaType getContentType(Operation operation); + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 3f31ef76e..7ecbf9ebc 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -1443,6 +1443,164 @@ public static ResponseFieldsSnippet relaxedResponseFields( true); } + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * request payload. + * + * @return the snippet that will document the request body + */ + public static RequestBodySnippet requestBody() { + return new RequestBodySnippet(); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * request payload. The given attributes will be made available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the request body + */ + public static RequestBodySnippet requestBody(Map attributes) { + return new RequestBodySnippet(attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's request payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the request body subsection + */ + public static RequestBodySnippet requestBody( + PayloadSubsectionExtractor subsectionExtractor) { + return new RequestBodySnippet(subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's request payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The given attributes will be made available during + * snippet generation. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the request body subsection + */ + public static RequestBodySnippet requestBody( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new RequestBodySnippet(subsectionExtractor, attributes); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * response payload. + * + * @return the snippet that will document the response body + */ + public static ResponseBodySnippet responseBody() { + return new ResponseBodySnippet(); + } + + /** + * Returns a {@code Snippet} that will document the body of the API operation's + * response payload. The given attributes will be made available during snippet + * generation. + * + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static ResponseBodySnippet responseBody(Map attributes) { + return new ResponseBodySnippet(attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the response body subsection + */ + public static ResponseBodySnippet responseBody( + PayloadSubsectionExtractor subsectionExtractor) { + return new ResponseBodySnippet(subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of the API + * operation's response payload. The subsection will be extracted using the given + * {@code subsectionExtractor}. The given attributes will be made available during + * snippet generation. + * + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the response body subsection + */ + public static ResponseBodySnippet responseBody( + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new ResponseBodySnippet(subsectionExtractor, attributes); + } + + /** + * Returns a {@code Snippet} that will document the body of specified part of the API + * operation's request payload. + * + * @param partName the name of the request part + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName) { + return new RequestPartBodySnippet(partName); + } + + /** + * Returns a {@code Snippet} that will document the body of specified part of the API + * operation's request payload. The given attributes will be made available during + * snippet generation. + * + * @param partName the name of the request part + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + Map attributes) { + return new RequestPartBodySnippet(partName, attributes); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of specified + * part of the API operation's request payload. The subsection will be extracted using + * the given {@code subsectionExtractor}. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + PayloadSubsectionExtractor subsectionExtractor) { + return new RequestPartBodySnippet(partName, subsectionExtractor); + } + + /** + * Returns a {@code Snippet} that will document a subsection of the body of specified + * part of the API operation's request payload. The subsection will be extracted using + * the given {@code subsectionExtractor}. The given attributes will be made available + * during snippet generation. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @param attributes the attributes + * @return the snippet that will document the response body + */ + public static RequestPartBodySnippet requestPartBody(String partName, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + return new RequestPartBodySnippet(partName, subsectionExtractor, attributes); + } + /** * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} * applied to their paths. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java new file mode 100644 index 000000000..5c6d3ea2e --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestBodySnippet.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the body of a request. + * + * @author Andy Wilkinson + */ +public class RequestBodySnippet extends AbstractBodySnippet { + + /** + * Creates a new {@code RequestBodySnippet}. + */ + public RequestBodySnippet() { + this(null, null); + } + + /** + * Creates a new {@code RequestBodySnippet} that will document the subsection of the + * request body extracted by the given {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + */ + public RequestBodySnippet(PayloadSubsectionExtractor subsectionExtractor) { + this(subsectionExtractor, null); + } + + /** + * Creates a new {@code RequestBodySnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + public RequestBodySnippet(Map attributes) { + this(null, attributes); + } + + /** + * Creates a new {@code RequestBodySnippet} that will document the subsection of the + * request body extracted by the given {@code subsectionExtractor}. The given + * additional {@code attributes} that will be included in the model during template + * rendering. + * + * @param subsectionExtractor the subsection extractor + * @param attributes The additional attributes + */ + public RequestBodySnippet(PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("request", subsectionExtractor, attributes); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return operation.getRequest().getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return operation.getRequest().getHeaders().getContentType(); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java new file mode 100644 index 000000000..0e5ec3243 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartBodySnippet.java @@ -0,0 +1,111 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the body of a request part. + * + * @author Andy Wilkinson + */ +public class RequestPartBodySnippet extends AbstractBodySnippet { + + private final String partName; + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. + * + * @param partName the name of the request part + */ + public RequestPartBodySnippet(String partName) { + this(partName, null, null); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the subsection of + * the body of the request part with the given {@code partName} extracted by the given + * {@code subsectionExtractor}. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + */ + public RequestPartBodySnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor) { + this(partName, subsectionExtractor, null); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. The given additional + * {@code attributes} will be included in the model during template rendering. + * + * @param partName the name of the request part + * @param attributes the additional attributes + */ + public RequestPartBodySnippet(String partName, Map attributes) { + this(partName, null, attributes); + } + + /** + * Creates a new {@code RequestPartBodySnippet} that will document the body of the + * request part with the given {@code partName}. The subsection of the body extracted + * by the given {@code subsectionExtractor} will be documented and the given + * additional {@code attributes} that will be included in the model during template + * rendering. + * + * @param partName the name of the request part + * @param subsectionExtractor the subsection extractor + * @param attributes The additional attributes + */ + public RequestPartBodySnippet(String partName, + PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("request-part-" + partName, "request-part", subsectionExtractor, + attributes); + this.partName = partName; + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return findPart(operation).getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return findPart(operation).getHeaders().getContentType(); + } + + private OperationRequestPart findPart(Operation operation) { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate; + } + } + throw new SnippetException("A request part named '" + this.partName + + "' was not found in the request"); + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java new file mode 100644 index 000000000..d0dbe1400 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseBodySnippet.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.snippet.Snippet; + +/** + * A {@link Snippet} that documents the body of a response. + * + * @author Andy Wilkinson + */ +public class ResponseBodySnippet extends AbstractBodySnippet { + + /** + * Creates a new {@code ResponseBodySnippet}. + */ + public ResponseBodySnippet() { + this(null, null); + } + + /** + * Creates a new {@code ResponseBodySnippet} that will document the subsection of the + * response body extracted by the given {@code subsectionExtractor}. + * + * @param subsectionExtractor the subsection extractor + */ + public ResponseBodySnippet(PayloadSubsectionExtractor subsectionExtractor) { + this(subsectionExtractor, null); + } + + /** + * Creates a new {@code ResponseBodySnippet} with the given additional + * {@code attributes} that will be included in the model during template rendering. + * + * @param attributes The additional attributes + */ + public ResponseBodySnippet(Map attributes) { + this(null, attributes); + } + + /** + * Creates a new {@code ResponseBodySnippet} that will document the subsection of the + * response body extracted by the given {@code subsectionExtractor}. The given + * additional {@code attributes} that will be included in the model during template + * rendering. + * + * @param subsectionExtractor the subsection extractor + * @param attributes The additional attributes + */ + public ResponseBodySnippet(PayloadSubsectionExtractor subsectionExtractor, + Map attributes) { + super("response", subsectionExtractor, attributes); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return operation.getResponse().getContent(); + } + + @Override + protected MediaType getContentType(Operation operation) { + return operation.getResponse().getHeaders().getContentType(); + } + +} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet new file mode 100644 index 000000000..e6cb8e9bd --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-body.snippet @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet new file mode 100644 index 000000000..e6cb8e9bd --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-body.snippet @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet new file mode 100644 index 000000000..e6cb8e9bd --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-response-body.snippet @@ -0,0 +1,4 @@ +[source,options="nowrap"] +---- +{{body}} +---- \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet new file mode 100644 index 000000000..b897d4095 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-body.snippet @@ -0,0 +1,3 @@ +``` +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet new file mode 100644 index 000000000..b897d4095 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-body.snippet @@ -0,0 +1,3 @@ +``` +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet new file mode 100644 index 000000000..b897d4095 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-response-body.snippet @@ -0,0 +1,3 @@ +``` +{{body}} +``` \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java index dbca4c0e9..014caa4d7 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/AbstractSnippetTests.java @@ -68,7 +68,11 @@ public AbstractSnippetTests(String name, TemplateFormat templateFormat) { } public CodeBlockMatcher codeBlock(String language) { - return SnippetMatchers.codeBlock(this.templateFormat, language); + return this.codeBlock(language, null); + } + + public CodeBlockMatcher codeBlock(String language, String options) { + return SnippetMatchers.codeBlock(this.templateFormat, language, options); } public TableMatcher tableWithHeader(String... headers) { diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java index 995a6ffb6..4cc0b635f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/config/RestDocumentationConfigurerTests.java @@ -31,6 +31,8 @@ import org.springframework.restdocs.generate.RestDocumentationGenerator; import org.springframework.restdocs.http.HttpRequestSnippet; import org.springframework.restdocs.http.HttpResponseSnippet; +import org.springframework.restdocs.payload.RequestBodySnippet; +import org.springframework.restdocs.payload.ResponseBodySnippet; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.StandardWriterResolver; import org.springframework.restdocs.snippet.WriterResolver; @@ -76,7 +78,9 @@ public void defaultConfiguration() { contains(instanceOf(CurlRequestSnippet.class), instanceOf(HttpieRequestSnippet.class), instanceOf(HttpRequestSnippet.class), - instanceOf(HttpResponseSnippet.class))); + instanceOf(HttpResponseSnippet.class), + instanceOf(RequestBodySnippet.class), + instanceOf(ResponseBodySnippet.class))); assertThat(configuration, hasEntry(equalTo(SnippetConfiguration.class.getName()), instanceOf(SnippetConfiguration.class))); SnippetConfiguration snippetConfiguration = (SnippetConfiguration) configuration @@ -138,7 +142,9 @@ public void additionalDefaultSnippets() { contains(instanceOf(CurlRequestSnippet.class), instanceOf(HttpieRequestSnippet.class), instanceOf(HttpRequestSnippet.class), - instanceOf(HttpResponseSnippet.class), equalTo(snippet))); + instanceOf(HttpResponseSnippet.class), + instanceOf(RequestBodySnippet.class), + instanceOf(ResponseBodySnippet.class), equalTo(snippet))); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java new file mode 100644 index 000000000..f19daee43 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodyPartSnippetTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestPartBodySnippet}. + * + * @author Andy Wilkinson + */ +public class RequestBodyPartSnippetTests extends AbstractSnippetTests { + + public RequestBodyPartSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void requestPartWithBody() throws IOException { + this.snippets.expect("request-part-one-body") + .withContents(codeBlock(null, "nowrap").content("some content")); + + requestPartBody("one").document(this.operationBuilder.request("http://localhost") + .part("one", "some content".getBytes()).build()); + } + + @Test + public void requestPartWithNoBody() throws IOException { + this.snippets.expect("request-part-one-body") + .withContents(codeBlock(null, "nowrap").content("")); + requestPartBody("one").document(this.operationBuilder.request("http://localhost") + .part("one", new byte[0]).build()); + } + + @Test + public void subsectionOfRequestPartBody() throws IOException { + this.snippets.expect("request-part-one-body-beneath-a.b") + .withContents(codeBlock(null, "nowrap").content("{\"c\":5}")); + + requestPartBody("one", beneathPath("a.b")) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\":{\"b\":{\"c\":5}}}".getBytes()).build()); + } + + @Test + public void customSnippetAttributes() throws IOException { + this.snippets.expect("request-part-one-body") + .withContents(codeBlock("json", "nowrap").content("{\"a\":\"alpha\"}")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-part-body")) + .willReturn(snippetResource("request-part-body-with-language")); + requestPartBody("one", + attributes( + key("language").value("json"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .part("one", + "{\"a\":\"alpha\"}".getBytes()) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java new file mode 100644 index 000000000..e6bde76d8 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestBodySnippetTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link RequestBodySnippet}. + * + * @author Andy Wilkinson + */ +public class RequestBodySnippetTests extends AbstractSnippetTests { + + public RequestBodySnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void requestWithBody() throws IOException { + this.snippets.expect("request-body") + .withContents(codeBlock(null, "nowrap").content("some content")); + + requestBody().document(this.operationBuilder.request("http://localhost") + .content("some content").build()); + } + + @Test + public void requestWithNoBody() throws IOException { + this.snippets.expect("request-body") + .withContents(codeBlock(null, "nowrap").content("")); + requestBody().document(this.operationBuilder.request("http://localhost").build()); + } + + @Test + public void subsectionOfRequestBody() throws IOException { + this.snippets.expect("request-body-beneath-a.b") + .withContents(codeBlock(null, "nowrap").content("{\"c\":5}")); + + requestBody(beneathPath("a.b")) + .document(this.operationBuilder.request("http://localhost") + .content("{\"a\":{\"b\":{\"c\":5}}}").build()); + } + + @Test + public void customSnippetAttributes() throws IOException { + this.snippets.expect("request-body") + .withContents(codeBlock("json", "nowrap").content("{\"a\":\"alpha\"}")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("request-body")) + .willReturn(snippetResource("request-body-with-language")); + requestBody( + attributes( + key("language").value("json"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .request("http://localhost") + .content("{\"a\":\"alpha\"}").build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java new file mode 100644 index 000000000..2b42539c0 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseBodySnippetTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2014-2016 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 java.io.IOException; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateFormat; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseBody; +import static org.springframework.restdocs.snippet.Attributes.attributes; +import static org.springframework.restdocs.snippet.Attributes.key; + +/** + * Tests for {@link ResponseBodySnippet}. + * + * @author Andy Wilkinson + */ +public class ResponseBodySnippetTests extends AbstractSnippetTests { + + public ResponseBodySnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void responseWithBody() throws IOException { + this.snippets.expect("response-body") + .withContents(codeBlock(null, "nowrap").content("some content")); + + PayloadDocumentation.responseBody().document( + this.operationBuilder.response().content("some content").build()); + } + + @Test + public void responseWithNoBody() throws IOException { + this.snippets.expect("response-body") + .withContents(codeBlock(null, "nowrap").content("")); + PayloadDocumentation.responseBody() + .document(this.operationBuilder.response().build()); + } + + @Test + public void subsectionOfResponseBody() throws IOException { + this.snippets.expect("response-body-beneath-a.b") + .withContents(codeBlock(null, "nowrap").content("{\"c\":5}")); + + responseBody(beneathPath("a.b")).document(this.operationBuilder.response() + .content("{\"a\":{\"b\":{\"c\":5}}}").build()); + } + + @Test + public void customSnippetAttributes() throws IOException { + this.snippets.expect("response-body") + .withContents(codeBlock("json", "nowrap").content("{\"a\":\"alpha\"}")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + given(resolver.resolveTemplateResource("response-body")) + .willReturn(snippetResource("response-body-with-language")); + responseBody( + attributes( + key("language").value("json"))) + .document( + this.operationBuilder + .attribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine( + resolver)) + .response().content("{\"a\":\"alpha\"}") + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index 287c933f8..c5e8da20d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -95,6 +95,15 @@ public static CodeBlockMatcher codeBlock(TemplateFormat format, String langua return new MarkdownCodeBlockMatcher(language); } + @SuppressWarnings({ "rawtypes" }) + public static CodeBlockMatcher codeBlock(TemplateFormat format, String language, + String options) { + if ("adoc".equals(format.getFileExtension())) { + return new AsciidoctorCodeBlockMatcher(language, options); + } + return new MarkdownCodeBlockMatcher(language); + } + private static abstract class AbstractSnippetContentMatcher extends BaseMatcher { @@ -186,7 +195,7 @@ public static class AsciidoctorCodeBlockMatcher Date: Fri, 16 Dec 2016 14:58:06 +0100 Subject: [PATCH 199/898] Clarify that parameterized output directory uses the simple class name Closes gh-334 --- docs/src/docs/asciidoc/documenting-your-api.adoc | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index d1a27442b..016aab3f6 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -805,13 +805,13 @@ are supported: | The name of the test method, formatted using snake_case | {ClassName} -| The unmodified name of the test class +| The unmodified simple name of the test class | {class-name} -| The name of the test class, formatted using kebab-case +| The simple name of the test class, formatted using kebab-case | {class_name} -| The nome of the test class, formatted using snake_case +| The simple name of the test class, formatted using snake_case | {step} | The count of calls made to the service in the current test @@ -924,6 +924,4 @@ and adds a title: ---- <1> Add a title to the table <2> Add a new column named "Constraints" -<3> Include the descriptors' `constraints` attribute in each row of the table - - +<3> Include the descriptors' `constraints` attribute in each row of the table \ No newline at end of file From 2fb3ede3a6073f8ca702dff1098298d5eddf3261 Mon Sep 17 00:00:00 2001 From: Gerrit Meier Date: Thu, 3 Nov 2016 06:40:59 +0100 Subject: [PATCH 200/898] Document that port is omitted when it matches the scheme's default Closes gh-325 Closes gh-329 --- docs/src/docs/asciidoc/configuration.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/docs/asciidoc/configuration.adoc b/docs/src/docs/asciidoc/configuration.adoc index 00c31ac61..178ac7b07 100644 --- a/docs/src/docs/asciidoc/configuration.adoc +++ b/docs/src/docs/asciidoc/configuration.adoc @@ -34,6 +34,9 @@ to change one or more of the defaults to suit your needs: include::{examples-dir}/com/example/mockmvc/CustomUriConfiguration.java[tags=custom-uri-configuration] ---- +NOTE: If the port is set to the default for the configured scheme (port 80 for HTTP or +port 443 for HTTPS), it will be omitted from any URIs in the generated snippets. + TIP: To configure a request's context path, use the `contextPath` method on `MockHttpServletRequestBuilder`. From 677a23647fdd2502843f296fbd9cc1b2f2dd45d0 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Jan 2017 19:57:29 +0000 Subject: [PATCH 201/898] Compile and test against Spring Framework 4.3.6.RELEASE This commit upgrade to Spring Framework 4.3.6.RELEASE. A side-effect of this change is that the Content-Type header is now always the first header when building a request with MockMvc. One test has been updated to reflect this change in ordering. Closes gh-342 --- build.gradle | 2 +- ...kMvcRestDocumentationIntegrationTests.java | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/build.gradle b/build.gradle index 7750213db..b4f341154 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ sonarqube { } ext { - springVersion = '4.3.1.RELEASE' + springVersion = '4.3.6.RELEASE' javadocLinks = [ 'http://docs.oracle.com/javase/8/docs/api/', "http://docs.spring.io/spring-framework/docs/$springVersion/javadoc-api/", diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 26ea2c958..cd47c51fe 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -401,26 +401,30 @@ public void preprocessedRequest() throws Exception { assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet(asciidoctor()) - .withContents( - httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("a", "alpha").header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", - MediaType.APPLICATION_JSON_VALUE) + is(snippet( + asciidoctor()) + .withContents( + httpRequest(asciidoctor(), RequestMethod.GET, "/") + .header("Content-Type", + "application/json") + .header("a", "alpha").header("b", "bravo") + .header("Accept", + MediaType.APPLICATION_JSON_VALUE) .header("Host", "localhost:8080") .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); - assertThat( - new File( - "build/generated-snippets/preprocessed-request/http-request.adoc"), - is(snippet(asciidoctor()) - .withContents(httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .content(prettyPrinted)))); + assertThat(new File( + "build/generated-snippets/preprocessed-request/http-request.adoc"), is( + snippet(asciidoctor()) + .withContents( + httpRequest(asciidoctor(), RequestMethod.GET, "/") + .header("Content-Type", + "application/json") + .header("b", "bravo") + .header("Accept", + MediaType.APPLICATION_JSON_VALUE) + .content(prettyPrinted)))); } @Test From 7e4a865d1bccb38286ee053b7e914609deda6df2 Mon Sep 17 00:00:00 2001 From: Florian Benz Date: Mon, 30 Jan 2017 12:34:25 +0100 Subject: [PATCH 202/898] Add spring-auto-restdocs to third-party extensions Closes gh-343 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c9b178feb..349347f8b 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ There are several that you can contribute to Spring REST Docs: | ---- | ----------- | | [restdocs-wiremock][17] | Auto-generate [WireMock][18] stubs as part of documenting your RESTful API | | [restdocsext-jersey][19] | Enables Spring REST Docs to be used with [Jersey's test framework][20] | +| [spring-auto-restdocs][21] | Uses introspection and Javadoc to automatically document request and response parameters | ## Licence @@ -63,4 +64,5 @@ Spring REST Docs is open source software released under the [Apache 2.0 license] [17]: https://github.com/ePages-de/restdocs-wiremock [18]: http://wiremock.org/ [19]: https://github.com/RESTDocsEXT/restdocsext-jersey -[20]: https://jersey.java.net/documentation/latest/test-framework.html \ No newline at end of file +[20]: https://jersey.java.net/documentation/latest/test-framework.html +[21]: https://github.com/ScaCap/spring-auto-restdocs \ No newline at end of file From 25bc6c37dae372a15dab59eee6acd252a0176968 Mon Sep 17 00:00:00 2001 From: Tomasz Kopczynski Date: Sun, 18 Dec 2016 15:43:58 +0100 Subject: [PATCH 203/898] Include cookies in request snippets Closes gh-336 See gh-302, gh-303, gh-304 --- .../restdocs/cli/CliOperationRequest.java | 7 ++++ .../restdocs/cli/CurlRequestSnippet.java | 18 +++++++++ .../restdocs/cli/HttpieRequestSnippet.java | 6 +++ .../restdocs/http/HttpRequestSnippet.java | 8 ++++ .../restdocs/operation/OperationRequest.java | 9 +++++ .../operation/OperationRequestFactory.java | 34 ++++++++++++++--- .../operation/StandardOperationRequest.java | 13 ++++++- .../restdocs/cli/CurlRequestSnippetTests.java | 11 ++++++ .../cli/HttpieRequestSnippetTests.java | 11 ++++++ .../headers/RequestHeadersSnippetTests.java | 7 +++- .../http/HttpRequestSnippetTests.java | 15 ++++++++ .../restdocs/test/OperationBuilder.java | 12 +++++- .../mockmvc/MockMvcRequestConverter.java | 15 +++++++- .../mockmvc/MockMvcRequestConverterTests.java | 23 ++++++++++++ ...kMvcRestDocumentationIntegrationTests.java | 37 +++++++++++++++++++ .../RestAssuredRequestConverter.java | 14 ++++++- .../RestAssuredRequestConverterTests.java | 23 ++++++++++++ ...uredRestDocumentationIntegrationTests.java | 19 ++++++++++ 18 files changed, 271 insertions(+), 11 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 2dde09ef6..22a6b2a4c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -25,6 +25,8 @@ import java.util.Map.Entry; import java.util.Set; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.OperationRequest; @@ -125,6 +127,11 @@ public URI getUri() { return this.delegate.getUri(); } + @Override + public Collection getCookies() { + return this.delegate.getCookies(); + } + private interface HeaderFilter { boolean allow(String name, List value); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 4f8d603cf..508097906 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.Map.Entry; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; @@ -92,12 +94,28 @@ private String getOptions(Operation operation) { writeUserOptionIfNecessary(request, printer); writeHttpMethodIfNecessary(request, printer); writeHeaders(request, printer); + writeCookies(request, printer); writePartsIfNecessary(request, printer); writeContent(request, printer); return command.toString(); } + private void writeCookies(CliOperationRequest request, PrintWriter printer) { + if (request.getCookies() != null && request.getCookies().size() > 0) { + printer.print(" --cookie "); + StringBuilder cookiesBuilder = new StringBuilder(); + + for (Cookie cookie : request.getCookies()) { + cookiesBuilder.append(String.format("%s=%s;", cookie.getName(), cookie.getValue())); + } + + String cookiesHeader = cookiesBuilder.substring(0, cookiesBuilder.length() - 1); // remove trailing semicolon + + printer.print(String.format("'%s'", cookiesHeader)); // add single quotes + } + } + private void writeIncludeHeadersInOutputOption(PrintWriter writer) { writer.print("-i"); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 10b96facb..6fa07f053 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.Map.Entry; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -160,6 +162,10 @@ private void writeHeaders(OperationRequest request, PrintWriter writer) { writer.print(String.format(" '%s:%s'", entry.getKey(), header)); } } + + for (Cookie cookie : request.getCookies()) { + writer.print(String.format(" 'Cookie:%s=%s'", cookie.getName(), cookie.getValue())); + } } private void writeParametersIfNecessary(CliOperationRequest request, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 0110dd1b6..8262571ae 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.Map.Entry; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -35,6 +37,7 @@ import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; + /** * A {@link Snippet} that documents an HTTP request. * @@ -112,6 +115,11 @@ private List> getHeaders(OperationRequest request) { } } + + for (Cookie cookie : request.getCookies()) { + headers.add(header(HttpHeaders.COOKIE, String.format("%s=%s", cookie.getName(), cookie.getValue()))); + } + if (requiresFormEncodingContentTypeHeader(request)) { headers.add(header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index 29153eec6..50d0e7bd9 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -19,6 +19,8 @@ import java.net.URI; import java.util.Collection; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -86,4 +88,11 @@ public interface OperationRequest { */ URI getUri(); + /** + * Returns cookies sent with the request. + * + * @return the cookies + */ + Collection getCookies(); + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 198011b45..3f75e289b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -18,6 +18,9 @@ import java.net.URI; import java.util.Collection; +import java.util.Collections; + +import javax.servlet.http.Cookie; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -40,13 +43,34 @@ public class OperationRequestFactory { * @param headers the request's headers * @param parameters the request's parameters * @param parts the request's parts + * @param cookies the request's cookies * @return the {@code OperationRequest} */ public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts) { + Collection parts, + Collection cookies) { return new StandardOperationRequest(uri, method, content, - augmentHeaders(headers, uri, content), parameters, parts); + augmentHeaders(headers, uri, content), parameters, parts, cookies); + } + + /** + * Creates a new {@link OperationRequest}. The given {@code headers} will be augmented + * to ensure that they always include a {@code Content-Length} header if the request + * has any content and a {@code Host} header. + * + * @param uri the request's uri + * @param method the request method + * @param content the content of the request + * @param headers the request's headers + * @param parameters the request's parameters + * @param parts the request's parts + * @return the {@code OperationRequest} + */ + public OperationRequest create(URI uri, HttpMethod method, byte[] content, + HttpHeaders headers, Parameters parameters, + Collection parts) { + return create(uri, method, content, headers, parameters, parts, Collections.emptyList()); } /** @@ -62,7 +86,7 @@ public OperationRequest create(URI uri, HttpMethod method, byte[] content, public OperationRequest createFrom(OperationRequest original, byte[] newContent) { return new StandardOperationRequest(original.getUri(), original.getMethod(), newContent, getUpdatedHeaders(original.getHeaders(), newContent), - original.getParameters(), original.getParts()); + original.getParameters(), original.getParts(), original.getCookies()); } /** @@ -78,7 +102,7 @@ public OperationRequest createFrom(OperationRequest original, HttpHeaders newHeaders) { return new StandardOperationRequest(original.getUri(), original.getMethod(), original.getContent(), newHeaders, original.getParameters(), - original.getParts()); + original.getParts(), original.getCookies()); } /** @@ -94,7 +118,7 @@ public OperationRequest createFrom(OperationRequest original, Parameters newParameters) { return new StandardOperationRequest(original.getUri(), original.getMethod(), original.getContent(), original.getHeaders(), newParameters, - original.getParts()); + original.getParts(), original.getCookies()); } private HttpHeaders augmentHeaders(HttpHeaders originalHeaders, URI uri, diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index a09bd645f..594534ce4 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -20,6 +20,8 @@ import java.util.Collection; import java.util.Collections; +import javax.servlet.http.Cookie; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -39,6 +41,8 @@ class StandardOperationRequest extends AbstractOperationMessage private URI uri; + private Collection cookies; + /** * Creates a new request with the given {@code uri} and {@code method}. The request * will have the given {@code headers}, {@code parameters}, and {@code parts}. @@ -52,12 +56,14 @@ class StandardOperationRequest extends AbstractOperationMessage */ StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts) { + Collection parts, + Collection cookies) { super(content, headers); this.uri = uri; this.method = method; this.parameters = parameters; this.parts = parts; + this.cookies = cookies; } @Override @@ -80,4 +86,9 @@ public URI getUri() { return this.uri; } + @Override + public Collection getCookies() { + return this.cookies; + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index fc4cf64c7..b5a23a5a9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -251,6 +251,17 @@ public void requestWithHeaders() throws IOException { .header("a", "alpha").build()); } + @Test + public void requestWithCookies() throws IOException { + this.snippets.expectCurlRequest() + .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + + " --cookie 'name1=value1;name2=value2'")); + new CurlRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .cookie("name1", "value1") + .cookie("name2", "value2").build()); + } + @Test public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index d5dc8a399..8cf564d58 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -252,6 +252,17 @@ public void requestWithHeaders() throws IOException { .header("a", "alpha").build()); } + @Test + public void requestWithCookies() throws IOException { + this.snippets.expectHttpieRequest().withContents( + codeBlock("bash").content("$ http GET 'http://localhost/foo'" + + " 'Cookie:name1=value1' 'Cookie:name2=value2'")); + new HttpieRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .cookie("name1", "value1") + .cookie("name2", "value2").build()); + } + @Test public void multipartPostWithNoSubmittedFileName() throws IOException { String expectedContent = String diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index ef87c2f72..d5c73bcb0 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -54,7 +54,7 @@ public void requestWithHeaders() throws IOException { .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") .row("`Accept-Language`", "four").row("`Cache-Control`", "five") - .row("`Connection`", "six")); + .row("`Connection`", "six").row("`Cookie`", "seven")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), @@ -63,7 +63,8 @@ public void requestWithHeaders() throws IOException { headerWithName("Cache-Control").description("five"), headerWithName( "Connection") - .description("six"))) + .description("six"), + headerWithName("Cookie").description("seven"))) .document( this.operationBuilder .request( @@ -78,6 +79,8 @@ public void requestWithHeaders() throws IOException { "max-age=0") .header("Connection", "keep-alive") + .header("Cookie", + "cookie1=cookieVal1; cookie2=cookieVal2") .build()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 88c27ee1e..2e64467e2 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -80,6 +80,21 @@ public void getRequestWithPort() throws IOException { .request("http://localhost:8080/foo").header("Alpha", "a").build()); } + @Test + public void getRequestWithCookies() throws IOException { + this.snippets.expectHttpRequest() + .withContents(httpRequest(RequestMethod.GET, "/foo") + .header(HttpHeaders.HOST, "localhost") + .header(HttpHeaders.COOKIE, "name1=value1") + .header(HttpHeaders.COOKIE, "name2=value2")); + + new HttpRequestSnippet() + .document(this.operationBuilder.request("http://localhost/foo") + .cookie("name1", "value1") + .cookie("name2", "value2") + .build()); + } + @Test public void getRequestWithQueryString() throws IOException { this.snippets.expectHttpRequest() diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 698a80dac..4bfa58af3 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -19,11 +19,14 @@ import java.io.File; import java.net.URI; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.servlet.http.Cookie; + import org.junit.runners.model.Statement; import org.springframework.http.HttpHeaders; @@ -153,6 +156,8 @@ public final class OperationRequestBuilder { private List partBuilders = new ArrayList<>(); + private Collection cookies = new ArrayList<>(); + private OperationRequestBuilder(String uri) { this.requestUri = URI.create(uri); } @@ -163,7 +168,7 @@ private OperationRequest buildRequest() { parts.add(builder.buildPart()); } return new OperationRequestFactory().create(this.requestUri, this.method, - this.content, this.headers, this.parameters, parts); + this.content, this.headers, this.parameters, parts, this.cookies); } public Operation build() { @@ -209,6 +214,11 @@ public OperationRequestPartBuilder part(String name, byte[] content) { return partBuilder; } + public OperationRequestBuilder cookie(String name, String value) { + this.cookies.add(new Cookie(name, value)); + return this; + } + /** * Basic builder API for creating an {@link OperationRequestPart}. */ diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index e933a0e0a..2d86da0a6 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -21,10 +21,14 @@ import java.io.StringWriter; import java.net.URI; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map.Entry; import javax.servlet.ServletException; +import javax.servlet.http.Cookie; import javax.servlet.http.Part; import org.springframework.http.HttpHeaders; @@ -67,6 +71,7 @@ public OperationRequest convert(MockHttpServletRequest mockRequest) { HttpHeaders headers = extractHeaders(mockRequest); Parameters parameters = extractParameters(mockRequest); List parts = extractParts(mockRequest); + Collection cookies = extractCookies(mockRequest); String queryString = mockRequest.getQueryString(); if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { @@ -78,13 +83,21 @@ public OperationRequest convert(MockHttpServletRequest mockRequest) { ? "?" + queryString : "")), HttpMethod.valueOf(mockRequest.getMethod()), FileCopyUtils.copyToByteArray(mockRequest.getInputStream()), headers, - parameters, parts); + parameters, parts, cookies); } catch (Exception ex) { throw new ConversionException(ex); } } + private Collection extractCookies(MockHttpServletRequest mockRequest) { + if (mockRequest.getCookies() != null) { + return Arrays.asList(mockRequest.getCookies()); + } + + return Collections.emptyList(); + } + private List extractParts(MockHttpServletRequest servletRequest) throws IOException, ServletException { List parts = new ArrayList<>(); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index 9304dd515..501752a57 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -19,7 +19,9 @@ import java.io.ByteArrayInputStream; import java.net.URI; import java.util.Arrays; +import java.util.Iterator; +import javax.servlet.http.Cookie; import javax.servlet.http.Part; import org.junit.Test; @@ -88,6 +90,27 @@ public void requestWithHeaders() throws Exception { assertThat(request.getHeaders(), hasEntry("b", Arrays.asList("bravo"))); } + @Test + public void requestWithCookies() throws Exception { + OperationRequest request = createOperationRequest(MockMvcRequestBuilders + .get("/foo") + .cookie(new Cookie("cookieName1", "cookieVal1"), + new Cookie("cookieName2", "cookieVal2"))); + assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); + assertThat(request.getMethod(), is(HttpMethod.GET)); + assertThat(request.getCookies().size(), is(equalTo(2))); + + Iterator cookieIterator = request.getCookies().iterator(); + + Cookie cookie1 = cookieIterator.next(); + assertThat(cookie1.getName(), is(equalTo("cookieName1"))); + assertThat(cookie1.getValue(), is(equalTo("cookieVal1"))); + + Cookie cookie2 = cookieIterator.next(); + assertThat(cookie2.getName(), is(equalTo("cookieName2"))); + assertThat(cookie2.getValue(), is(equalTo("cookieVal2"))); + } + @Test public void httpsRequest() throws Exception { MockHttpServletRequest mockRequest = MockMvcRequestBuilders.get("/foo") diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index cd47c51fe..a7e563140 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.regex.Pattern; +import javax.servlet.http.Cookie; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -159,6 +161,23 @@ public void curlSnippetWithContent() throws Exception { + "-H 'Accept: application/json' -d 'content'")))); } + @Test + public void curlSnippetWithCookies() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(get("/") + .accept(MediaType.APPLICATION_JSON) + .cookie(new Cookie("cookieName", "cookieVal"))) + .andExpect(status().isOk()).andDo(document("curl-snippet-with-cookies")); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ curl " + "'http://localhost:8080/' -i " + + "-H 'Accept: application/json' --cookie 'cookieName=cookieVal'")))); + } + @Test public void curlSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) @@ -207,6 +226,24 @@ public void httpieSnippetWithContent() throws Exception { + " 'Accept:application/json'")))); } + @Test + public void httpieSnippetWithCookies() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform(get("/") + .accept(MediaType.APPLICATION_JSON) + .cookie(new Cookie("cookieName", "cookieVal"))) + .andExpect(status().isOk()) + .andDo(document("httpie-snippet-with-cookies")); + assertThat( + new File( + "build/generated-snippets/httpie-snippet-with-cookies/httpie-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ http GET 'http://localhost:8080/'" + + " 'Accept:application/json' 'Cookie:cookieName=cookieVal'")))); + } + @Test public void httpieSnippetWithQueryStringOnPost() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index 1673366a0..eff63a4c2 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map.Entry; +import com.jayway.restassured.response.Cookie; import com.jayway.restassured.response.Header; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.MultiPartSpecification; @@ -55,7 +56,18 @@ public OperationRequest convert(FilterableRequestSpecification requestSpec) { return new OperationRequestFactory().create(URI.create(requestSpec.getURI()), HttpMethod.valueOf(requestSpec.getMethod().name()), extractContent(requestSpec), extractHeaders(requestSpec), - extractParameters(requestSpec), extractParts(requestSpec)); + extractParameters(requestSpec), extractParts(requestSpec), + extractCookies(requestSpec)); + } + + private Collection extractCookies(FilterableRequestSpecification requestSpec) { + Collection cookies = new ArrayList<>(); + + for (Cookie cookie : requestSpec.getCookies()) { + cookies.add(new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue())); + } + + return cookies; } private byte[] extractContent(FilterableRequestSpecification requestSpec) { diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 562a82603..4db143211 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -25,6 +25,8 @@ import java.util.Collection; import java.util.Iterator; +import javax.servlet.http.Cookie; + import com.jayway.restassured.RestAssured; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.RequestSpecification; @@ -143,6 +145,27 @@ public void headers() { is(equalTo(Arrays.asList("localhost:" + this.port)))); } + @Test + public void cookies() { + RequestSpecification requestSpec = RestAssured.given().port(this.port) + .cookie("cookie1", "cookieVal1") + .cookie("cookie2", "cookieVal2"); + requestSpec.get("/"); + OperationRequest request = this.factory + .convert((FilterableRequestSpecification) requestSpec); + assertThat(request.getCookies().size(), is(equalTo(2))); + + Iterator cookieIterator = request.getCookies().iterator(); + Cookie cookie1 = cookieIterator.next(); + + assertThat(cookie1.getName(), is(equalTo("cookie1"))); + assertThat(cookie1.getValue(), is(equalTo("cookieVal1"))); + + Cookie cookie2 = cookieIterator.next(); + assertThat(cookie2.getName(), is(equalTo("cookie2"))); + assertThat(cookie2.getValue(), is(equalTo("cookieVal2"))); + } + @Test public void multipart() { RequestSpecification requestSpec = RestAssured.given().port(this.port) diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index da43824bc..087003d90 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -119,6 +119,25 @@ public void curlSnippetWithContent() throws Exception { + "-d 'content'")))); } + @Test + public void curlSnippetWithCookies() throws Exception { + String contentType = "text/plain; charset=UTF-8"; + given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + .filter(document("curl-snippet-with-cookies")).accept("application/json") + .contentType(contentType) + .cookie("cookieName", "cookieVal").get("/") + .then() + .statusCode(200); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc"), + is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") + .content("$ curl 'http://localhost:" + this.port + "/' -i " + + "-H 'Accept: application/json' " + + "-H 'Content-Type: " + contentType + "' " + + "--cookie 'cookieName=cookieVal'")))); + } + @Test public void curlSnippetWithQueryStringOnPost() throws Exception { given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) From a9c1e04081971d06eacef8109a47688e72753420 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 2 Feb 2017 14:42:43 +0000 Subject: [PATCH 204/898] Polish "Include cookies in request snippets" See gh-336 Closes gh-302 Closes gh-303 Closes gh-304 --- .../restdocs/cli/CliOperationRequest.java | 2 +- .../restdocs/cli/CurlRequestSnippet.java | 18 ++++---- .../restdocs/cli/HttpieRequestSnippet.java | 8 +++- .../restdocs/operation/OperationRequest.java | 8 ++-- .../operation/StandardOperationRequest.java | 9 ++-- .../restdocs/cli/CurlRequestSnippetTests.java | 34 +++++++-------- .../cli/HttpieRequestSnippetTests.java | 34 +++++++-------- .../headers/RequestHeadersSnippetTests.java | 21 ++++------ .../http/HttpRequestSnippetTests.java | 41 +++++++++---------- .../restdocs/test/OperationBuilder.java | 2 +- .../mockmvc/MockMvcRequestConverter.java | 2 +- .../mockmvc/MockMvcRequestConverterTests.java | 5 +-- ...kMvcRestDocumentationIntegrationTests.java | 13 +++--- .../RestAssuredRequestConverter.java | 10 ++--- .../RestAssuredRequestConverterTests.java | 5 +-- ...uredRestDocumentationIntegrationTests.java | 12 +++--- 16 files changed, 107 insertions(+), 117 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 22a6b2a4c..10ae0486c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 508097906..886a4c070 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -32,6 +32,7 @@ import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -102,17 +103,16 @@ private String getOptions(Operation operation) { } private void writeCookies(CliOperationRequest request, PrintWriter printer) { - if (request.getCookies() != null && request.getCookies().size() > 0) { - printer.print(" --cookie "); + if (!CollectionUtils.isEmpty(request.getCookies())) { StringBuilder cookiesBuilder = new StringBuilder(); - for (Cookie cookie : request.getCookies()) { - cookiesBuilder.append(String.format("%s=%s;", cookie.getName(), cookie.getValue())); + if (cookiesBuilder.length() > 0) { + cookiesBuilder.append(";"); + } + cookiesBuilder.append( + String.format("%s=%s", cookie.getName(), cookie.getValue())); } - - String cookiesHeader = cookiesBuilder.substring(0, cookiesBuilder.length() - 1); // remove trailing semicolon - - printer.print(String.format("'%s'", cookiesHeader)); // add single quotes + printer.print(String.format(" --cookie '%s'", cookiesBuilder.toString())); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 6fa07f053..38a8970be 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -108,6 +108,7 @@ private String getRequestItems(CliOperationRequest request) { PrintWriter printer = new PrintWriter(requestItems); writeFormDataIfNecessary(request, printer); writeHeaders(request, printer); + writeCookies(request, printer); writeParametersIfNecessary(request, printer); return requestItems.toString(); } @@ -162,9 +163,12 @@ private void writeHeaders(OperationRequest request, PrintWriter writer) { writer.print(String.format(" '%s:%s'", entry.getKey(), header)); } } + } + private void writeCookies(OperationRequest request, PrintWriter writer) { for (Cookie cookie : request.getCookies()) { - writer.print(String.format(" 'Cookie:%s=%s'", cookie.getName(), cookie.getValue())); + writer.print(String.format(" 'Cookie:%s=%s'", cookie.getName(), + cookie.getValue())); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index 50d0e7bd9..93972dba0 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2017 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. @@ -89,9 +89,11 @@ public interface OperationRequest { URI getUri(); /** - * Returns cookies sent with the request. + * Returns {@link Cookie Cookies} sent with the request. If no cookies were sent an + * empty collection is returned. * - * @return the cookies + * @return the cookies, never {@code null} + * @since 1.2.0 */ Collection getCookies(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index 594534ce4..8495c6c99 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2017 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. @@ -45,7 +45,8 @@ class StandardOperationRequest extends AbstractOperationMessage /** * Creates a new request with the given {@code uri} and {@code method}. The request - * will have the given {@code headers}, {@code parameters}, and {@code parts}. + * will have the given {@code headers}, {@code parameters}, {@code parts}, and + * {@code cookies}. * * @param uri the uri * @param method the method @@ -53,11 +54,11 @@ class StandardOperationRequest extends AbstractOperationMessage * @param headers the headers * @param parameters the parameters * @param parts the parts + * @param cookies the cookies */ StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts, - Collection cookies) { + Collection parts, Collection cookies) { super(content, headers); this.uri = uri; this.method = method; diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index b5a23a5a9..33d6aa88e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -254,12 +254,11 @@ public void requestWithHeaders() throws IOException { @Test public void requestWithCookies() throws IOException { this.snippets.expectCurlRequest() - .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + - " --cookie 'name1=value1;name2=value2'")); + .withContents(codeBlock("bash").content("$ curl 'http://localhost/foo' -i" + + " --cookie 'name1=value1;name2=value2'")); new CurlRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") - .cookie("name1", "value1") - .cookie("name2", "value2").build()); + .cookie("name1", "value1").cookie("name2", "value2").build()); } @Test @@ -269,9 +268,10 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { + "'metadata={\"description\": \"foo\"}'"; this.snippets.expectCurlRequest() .withContents(codeBlock("bash").content(expectedContent)); - new CurlRequestSnippet().document(this.operationBuilder - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + new CurlRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @@ -286,9 +286,9 @@ public void multipartPostWithContentType() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -302,8 +302,8 @@ public void multipartPost() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -318,9 +318,9 @@ public void multipartPostWithParameters() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); } @Test @@ -332,7 +332,7 @@ public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils .encodeToString("user:secret".getBytes())) - .build()); + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 8cf564d58..acb12cb96 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -255,12 +255,11 @@ public void requestWithHeaders() throws IOException { @Test public void requestWithCookies() throws IOException { this.snippets.expectHttpieRequest().withContents( - codeBlock("bash").content("$ http GET 'http://localhost/foo'" + - " 'Cookie:name1=value1' 'Cookie:name2=value2'")); + codeBlock("bash").content("$ http GET 'http://localhost/foo'" + + " 'Cookie:name1=value1' 'Cookie:name2=value2'")); new HttpieRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") - .cookie("name1", "value1") - .cookie("name2", "value2").build()); + .cookie("name1", "value1").cookie("name2", "value2").build()); } @Test @@ -270,9 +269,10 @@ public void multipartPostWithNoSubmittedFileName() throws IOException { + " 'metadata'@<(echo '{\"description\": \"foo\"}')"); this.snippets.expectHttpieRequest() .withContents(codeBlock("bash").content(expectedContent)); - new HttpieRequestSnippet().document(this.operationBuilder - .request("http://localhost/upload").method("POST") - .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + new HttpieRequestSnippet().document( + this.operationBuilder.request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, + MediaType.MULTIPART_FORM_DATA_VALUE) .part("metadata", "{\"description\": \"foo\"}".getBytes()).build()); } @@ -288,9 +288,9 @@ public void multipartPostWithContentType() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -304,8 +304,8 @@ public void multipartPost() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").build()); } @Test @@ -320,9 +320,9 @@ public void multipartPostWithParameters() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", new byte[0]) - .submittedFileName("documents/images/example.png").and() - .param("a", "apple", "avocado").param("b", "banana").build()); + .part("image", new byte[0]) + .submittedFileName("documents/images/example.png").and() + .param("a", "apple", "avocado").param("b", "banana").build()); } @Test @@ -334,7 +334,7 @@ public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException .header(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils .encodeToString("user:secret".getBytes())) - .build()); + .build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java index d5c73bcb0..1f28ea119 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/headers/RequestHeadersSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -54,7 +54,7 @@ public void requestWithHeaders() throws IOException { .row("`X-Test`", "one").row("`Accept`", "two") .row("`Accept-Encoding`", "three") .row("`Accept-Language`", "four").row("`Cache-Control`", "five") - .row("`Connection`", "six").row("`Cookie`", "seven")); + .row("`Connection`", "six")); new RequestHeadersSnippet( Arrays.asList(headerWithName("X-Test").description("one"), headerWithName("Accept").description("two"), @@ -63,8 +63,7 @@ public void requestWithHeaders() throws IOException { headerWithName("Cache-Control").description("five"), headerWithName( "Connection") - .description("six"), - headerWithName("Cookie").description("seven"))) + .description("six"))) .document( this.operationBuilder .request( @@ -73,15 +72,9 @@ public void requestWithHeaders() throws IOException { .header("Accept", "*/*") .header("Accept-Encoding", "gzip, deflate") - .header("Accept-Language", - "en-US,en;q=0.5") - .header("Cache-Control", - "max-age=0") - .header("Connection", - "keep-alive") - .header("Cookie", - "cookie1=cookieVal1; cookie2=cookieVal2") - .build()); + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cache-Control", "max-age=0") + .header("Connection", "keep-alive").build()); } @Test @@ -150,7 +143,7 @@ public void requestHeadersWithCustomDescriptorAttributes() throws IOException { .header("X-Test", "test") .header("Accept-Encoding", "gzip, deflate") - .header("Accept", "*/*").build()); + .header("Accept", "*/*").build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index 2e64467e2..4312770ee 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -90,9 +90,7 @@ public void getRequestWithCookies() throws IOException { new HttpRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") - .cookie("name1", "value1") - .cookie("name2", "value2") - .build()); + .cookie("name1", "value1").cookie("name2", "value2").build()); } @Test @@ -142,8 +140,8 @@ public void postRequestWithContent() throws IOException { String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("POST").content(content).build()); @@ -154,8 +152,8 @@ public void postRequestWithContentAndParameters() throws IOException { String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo") @@ -168,8 +166,8 @@ public void postRequestWithContentAndDisjointQueryStringAndParameters() String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet() .document(this.operationBuilder.request("http://localhost/foo?b=bravo") @@ -182,8 +180,8 @@ public void postRequestWithContentAndPartiallyOverlappingQueryStringAndParameter String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?b=bravo").method("POST") @@ -196,8 +194,8 @@ public void postRequestWithContentAndTotallyOverlappingQueryStringAndParameters( String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.POST, "/foo?b=bravo&a=alpha") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo?b=bravo&a=alpha").method("POST") @@ -251,8 +249,8 @@ public void putRequestWithContent() throws IOException { String content = "Hello, world"; this.snippets.expectHttpRequest() .withContents(httpRequest(RequestMethod.PUT, "/foo") - .header(HttpHeaders.HOST, "localhost").content(content).header( - HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); + .header(HttpHeaders.HOST, "localhost").content(content) + .header(HttpHeaders.CONTENT_LENGTH, content.getBytes().length)); new HttpRequestSnippet().document(this.operationBuilder .request("http://localhost/foo").method("PUT").content(content).build()); @@ -284,7 +282,7 @@ public void multipartPost() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()).build()); + .part("image", "<< data >>".getBytes()).build()); } @Test @@ -310,8 +308,8 @@ public void multipartPostWithParameters() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .param("a", "apple", "avocado").param("b", "banana") - .part("image", "<< data >>".getBytes()).build()); + .param("a", "apple", "avocado").param("b", "banana") + .part("image", "<< data >>".getBytes()).build()); } @Test @@ -347,9 +345,8 @@ public void multipartPostWithContentType() throws IOException { this.operationBuilder.request("http://localhost/upload").method("POST") .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) - .part("image", "<< data >>".getBytes()) - .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE) - .build()); + .part("image", "<< data >>".getBytes()) + .header(HttpHeaders.CONTENT_TYPE, MediaType.IMAGE_PNG_VALUE).build()); } @Test diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 4bfa58af3..6d210b5f1 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index 2d86da0a6..2aeb6119a 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index 501752a57..d2a18373c 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -93,8 +93,7 @@ public void requestWithHeaders() throws Exception { @Test public void requestWithCookies() throws Exception { OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo") - .cookie(new Cookie("cookieName1", "cookieVal1"), + .get("/foo").cookie(new Cookie("cookieName1", "cookieVal1"), new Cookie("cookieName2", "cookieVal2"))); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index a7e563140..77b2136a2 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -166,10 +166,9 @@ public void curlSnippetWithCookies() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(get("/") - .accept(MediaType.APPLICATION_JSON) - .cookie(new Cookie("cookieName", "cookieVal"))) - .andExpect(status().isOk()).andDo(document("curl-snippet-with-cookies")); + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON) + .cookie(new Cookie("cookieName", "cookieVal"))).andExpect(status().isOk()) + .andDo(document("curl-snippet-with-cookies")); assertThat( new File( "build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc"), @@ -231,10 +230,8 @@ public void httpieSnippetWithCookies() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) .apply(documentationConfiguration(this.restDocumentation)).build(); - mockMvc.perform(get("/") - .accept(MediaType.APPLICATION_JSON) - .cookie(new Cookie("cookieName", "cookieVal"))) - .andExpect(status().isOk()) + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON) + .cookie(new Cookie("cookieName", "cookieVal"))).andExpect(status().isOk()) .andDo(document("httpie-snippet-with-cookies")); assertThat( new File( diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index eff63a4c2..6b64c6a3c 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -60,13 +60,13 @@ public OperationRequest convert(FilterableRequestSpecification requestSpec) { extractCookies(requestSpec)); } - private Collection extractCookies(FilterableRequestSpecification requestSpec) { + private Collection extractCookies( + FilterableRequestSpecification requestSpec) { Collection cookies = new ArrayList<>(); - for (Cookie cookie : requestSpec.getCookies()) { - cookies.add(new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue())); + cookies.add( + new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue())); } - return cookies; } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 4db143211..4617cb30d 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -148,8 +148,7 @@ public void headers() { @Test public void cookies() { RequestSpecification requestSpec = RestAssured.given().port(this.port) - .cookie("cookie1", "cookieVal1") - .cookie("cookie2", "cookieVal2"); + .cookie("cookie1", "cookieVal1").cookie("cookie2", "cookieVal2"); requestSpec.get("/"); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index 087003d90..07d62682b 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -124,17 +124,15 @@ public void curlSnippetWithCookies() throws Exception { String contentType = "text/plain; charset=UTF-8"; given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) .filter(document("curl-snippet-with-cookies")).accept("application/json") - .contentType(contentType) - .cookie("cookieName", "cookieVal").get("/") - .then() - .statusCode(200); + .contentType(contentType).cookie("cookieName", "cookieVal").get("/") + .then().statusCode(200); assertThat( new File( "build/generated-snippets/curl-snippet-with-cookies/curl-request.adoc"), is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") .content("$ curl 'http://localhost:" + this.port + "/' -i " - + "-H 'Accept: application/json' " - + "-H 'Content-Type: " + contentType + "' " + + "-H 'Accept: application/json' " + "-H 'Content-Type: " + + contentType + "' " + "--cookie 'cookieName=cookieVal'")))); } From 722048e20fe77956e4ddb1d148433c78b114acf3 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 3 Feb 2017 14:53:54 +0000 Subject: [PATCH 205/898] Introduce RequestCookie to avoid core depending on the Servlet API Closes gh-345 --- spring-restdocs-core/build.gradle | 1 - .../restdocs/cli/CliOperationRequest.java | 5 +- .../restdocs/cli/CurlRequestSnippet.java | 5 +- .../restdocs/cli/HttpieRequestSnippet.java | 5 +- .../restdocs/http/HttpRequestSnippet.java | 9 ++- .../restdocs/operation/OperationRequest.java | 8 +-- .../operation/OperationRequestFactory.java | 8 +-- .../restdocs/operation/RequestCookie.java | 60 +++++++++++++++++++ .../operation/StandardOperationRequest.java | 8 +-- .../restdocs/test/OperationBuilder.java | 7 +-- spring-restdocs-mockmvc/build.gradle | 1 + .../mockmvc/MockMvcRequestConverter.java | 19 +++--- .../mockmvc/MockMvcRequestConverterTests.java | 16 ++--- .../RestAssuredRequestConverter.java | 8 +-- .../RestAssuredRequestConverterTests.java | 9 ++- 15 files changed, 111 insertions(+), 58 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java diff --git a/spring-restdocs-core/build.gradle b/spring-restdocs-core/build.gradle index 4a8417072..60752f0b1 100644 --- a/spring-restdocs-core/build.gradle +++ b/spring-restdocs-core/build.gradle @@ -27,7 +27,6 @@ task jmustacheRepackJar(type: Jar) { repackJar -> dependencies { compile 'com.fasterxml.jackson.core:jackson-databind' compile 'org.springframework:spring-web' - compile 'javax.servlet:javax.servlet-api' compile files(jmustacheRepackJar) jarjar 'com.googlecode.jarjar:jarjar:1.3' jmustache 'com.samskivert:jmustache@jar' diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 10ae0486c..36bcb5913 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -25,13 +25,12 @@ import java.util.Map.Entry; import java.util.Set; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.Base64Utils; /** @@ -128,7 +127,7 @@ public URI getUri() { } @Override - public Collection getCookies() { + public Collection getCookies() { return this.delegate.getCookies(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index 886a4c070..1fb6140ac 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -23,13 +23,12 @@ import java.util.Map; import java.util.Map.Entry; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.CollectionUtils; @@ -105,7 +104,7 @@ private String getOptions(Operation operation) { private void writeCookies(CliOperationRequest request, PrintWriter printer) { if (!CollectionUtils.isEmpty(request.getCookies())) { StringBuilder cookiesBuilder = new StringBuilder(); - for (Cookie cookie : request.getCookies()) { + for (RequestCookie cookie : request.getCookies()) { if (cookiesBuilder.length() > 0) { cookiesBuilder.append(";"); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 38a8970be..410495fee 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -23,8 +23,6 @@ import java.util.Map; import java.util.Map.Entry; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -32,6 +30,7 @@ import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; @@ -166,7 +165,7 @@ private void writeHeaders(OperationRequest request, PrintWriter writer) { } private void writeCookies(OperationRequest request, PrintWriter writer) { - for (Cookie cookie : request.getCookies()) { + for (RequestCookie cookie : request.getCookies()) { writer.print(String.format(" 'Cookie:%s=%s'", cookie.getName(), cookie.getValue())); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 8262571ae..f72337af3 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -24,8 +24,6 @@ import java.util.Map; import java.util.Map.Entry; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -33,11 +31,11 @@ import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.StringUtils; - /** * A {@link Snippet} that documents an HTTP request. * @@ -116,8 +114,9 @@ private List> getHeaders(OperationRequest request) { } } - for (Cookie cookie : request.getCookies()) { - headers.add(header(HttpHeaders.COOKIE, String.format("%s=%s", cookie.getName(), cookie.getValue()))); + for (RequestCookie cookie : request.getCookies()) { + headers.add(header(HttpHeaders.COOKIE, + String.format("%s=%s", cookie.getName(), cookie.getValue()))); } if (requiresFormEncodingContentTypeHeader(request)) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java index 93972dba0..f768663dd 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequest.java @@ -19,8 +19,6 @@ import java.net.URI; import java.util.Collection; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -89,12 +87,12 @@ public interface OperationRequest { URI getUri(); /** - * Returns {@link Cookie Cookies} sent with the request. If no cookies were sent an - * empty collection is returned. + * Returns the {@link RequestCookie cookies} sent with the request. If no cookies were + * sent an empty collection is returned. * * @return the cookies, never {@code null} * @since 1.2.0 */ - Collection getCookies(); + Collection getCookies(); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java index 3f75e289b..da1c07cf5 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/OperationRequestFactory.java @@ -20,8 +20,6 @@ import java.util.Collection; import java.util.Collections; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -48,8 +46,7 @@ public class OperationRequestFactory { */ public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts, - Collection cookies) { + Collection parts, Collection cookies) { return new StandardOperationRequest(uri, method, content, augmentHeaders(headers, uri, content), parameters, parts, cookies); } @@ -70,7 +67,8 @@ public OperationRequest create(URI uri, HttpMethod method, byte[] content, public OperationRequest create(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, Collection parts) { - return create(uri, method, content, headers, parameters, parts, Collections.emptyList()); + return create(uri, method, content, headers, parameters, parts, + Collections.emptyList()); } /** diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java new file mode 100644 index 000000000..595a19f22 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/RequestCookie.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014-2017 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.operation; + +/** + * A representation of a Cookie received in a request. + * + * @author Andy Wilkinson + * @since 1.2.0 + */ +public final class RequestCookie { + + private final String name; + + private final String value; + + /** + * Creates a new {@code RequestCookie} with the given {@code name} and {@code value}. + * + * @param name the name of the cookie + * @param value the value of the cookie + */ + public RequestCookie(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Returns the name of the cookie. + * + * @return the name + */ + public String getName() { + return this.name; + } + + /** + * Returns the value of the cookie. + * + * @return the value + */ + public String getValue() { + return this.value; + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java index 8495c6c99..5fb575088 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/StandardOperationRequest.java @@ -20,8 +20,6 @@ import java.util.Collection; import java.util.Collections; -import javax.servlet.http.Cookie; - import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -41,7 +39,7 @@ class StandardOperationRequest extends AbstractOperationMessage private URI uri; - private Collection cookies; + private Collection cookies; /** * Creates a new request with the given {@code uri} and {@code method}. The request @@ -58,7 +56,7 @@ class StandardOperationRequest extends AbstractOperationMessage */ StandardOperationRequest(URI uri, HttpMethod method, byte[] content, HttpHeaders headers, Parameters parameters, - Collection parts, Collection cookies) { + Collection parts, Collection cookies) { super(content, headers); this.uri = uri; this.method = method; @@ -88,7 +86,7 @@ public URI getUri() { } @Override - public Collection getCookies() { + public Collection getCookies() { return this.cookies; } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java index 6d210b5f1..032b0845f 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/OperationBuilder.java @@ -25,8 +25,6 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.Cookie; - import org.junit.runners.model.Statement; import org.springframework.http.HttpHeaders; @@ -43,6 +41,7 @@ import org.springframework.restdocs.operation.OperationResponse; import org.springframework.restdocs.operation.OperationResponseFactory; import org.springframework.restdocs.operation.Parameters; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.operation.StandardOperation; import org.springframework.restdocs.snippet.RestDocumentationContextPlaceholderResolverFactory; import org.springframework.restdocs.snippet.StandardWriterResolver; @@ -156,7 +155,7 @@ public final class OperationRequestBuilder { private List partBuilders = new ArrayList<>(); - private Collection cookies = new ArrayList<>(); + private Collection cookies = new ArrayList<>(); private OperationRequestBuilder(String uri) { this.requestUri = URI.create(uri); @@ -215,7 +214,7 @@ public OperationRequestPartBuilder part(String name, byte[] content) { } public OperationRequestBuilder cookie(String name, String value) { - this.cookies.add(new Cookie(name, value)); + this.cookies.add(new RequestCookie(name, value)); return this; } diff --git a/spring-restdocs-mockmvc/build.gradle b/spring-restdocs-mockmvc/build.gradle index d8a0529cb..5baca9c25 100644 --- a/spring-restdocs-mockmvc/build.gradle +++ b/spring-restdocs-mockmvc/build.gradle @@ -1,6 +1,7 @@ description = 'Spring REST Docs MockMvc' dependencies { + compile 'javax.servlet:javax.servlet-api' compile 'org.springframework:spring-test' compile project(':spring-restdocs-core') optional 'junit:junit' diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index 2aeb6119a..af66e4017 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -21,14 +21,12 @@ import java.io.StringWriter; import java.net.URI; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map.Entry; import javax.servlet.ServletException; -import javax.servlet.http.Cookie; import javax.servlet.http.Part; import org.springframework.http.HttpHeaders; @@ -43,6 +41,7 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; @@ -71,7 +70,7 @@ public OperationRequest convert(MockHttpServletRequest mockRequest) { HttpHeaders headers = extractHeaders(mockRequest); Parameters parameters = extractParameters(mockRequest); List parts = extractParts(mockRequest); - Collection cookies = extractCookies(mockRequest); + Collection cookies = extractCookies(mockRequest); String queryString = mockRequest.getQueryString(); if (!StringUtils.hasText(queryString) && "GET".equals(mockRequest.getMethod())) { @@ -90,12 +89,16 @@ public OperationRequest convert(MockHttpServletRequest mockRequest) { } } - private Collection extractCookies(MockHttpServletRequest mockRequest) { - if (mockRequest.getCookies() != null) { - return Arrays.asList(mockRequest.getCookies()); + private Collection extractCookies(MockHttpServletRequest mockRequest) { + if (mockRequest.getCookies() == null || mockRequest.getCookies().length == 0) { + return Collections.emptyList(); } - - return Collections.emptyList(); + List cookies = new ArrayList<>(); + for (javax.servlet.http.Cookie servletCookie : mockRequest.getCookies()) { + cookies.add( + new RequestCookie(servletCookie.getName(), servletCookie.getValue())); + } + return cookies; } private List extractParts(MockHttpServletRequest servletRequest) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index d2a18373c..8b6c67eba 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -21,7 +21,6 @@ import java.util.Arrays; import java.util.Iterator; -import javax.servlet.http.Cookie; import javax.servlet.http.Part; import org.junit.Test; @@ -33,6 +32,7 @@ import org.springframework.mock.web.MockServletContext; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -92,20 +92,22 @@ public void requestWithHeaders() throws Exception { @Test public void requestWithCookies() throws Exception { - OperationRequest request = createOperationRequest(MockMvcRequestBuilders - .get("/foo").cookie(new Cookie("cookieName1", "cookieVal1"), - new Cookie("cookieName2", "cookieVal2"))); + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.get("/foo") + .cookie(new javax.servlet.http.Cookie("cookieName1", + "cookieVal1"), + new javax.servlet.http.Cookie("cookieName2", "cookieVal2"))); assertThat(request.getUri(), is(URI.create("http://localhost/foo"))); assertThat(request.getMethod(), is(HttpMethod.GET)); assertThat(request.getCookies().size(), is(equalTo(2))); - Iterator cookieIterator = request.getCookies().iterator(); + Iterator cookieIterator = request.getCookies().iterator(); - Cookie cookie1 = cookieIterator.next(); + RequestCookie cookie1 = cookieIterator.next(); assertThat(cookie1.getName(), is(equalTo("cookieName1"))); assertThat(cookie1.getValue(), is(equalTo("cookieVal1"))); - Cookie cookie2 = cookieIterator.next(); + RequestCookie cookie2 = cookieIterator.next(); assertThat(cookie2.getName(), is(equalTo("cookieName2"))); assertThat(cookie2.getValue(), is(equalTo("cookieVal2"))); } diff --git a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java index 6b64c6a3c..852db3a26 100644 --- a/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java +++ b/spring-restdocs-restassured/src/main/java/org/springframework/restdocs/restassured/RestAssuredRequestConverter.java @@ -39,6 +39,7 @@ import org.springframework.restdocs.operation.OperationRequestPartFactory; import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.operation.RequestConverter; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.util.FileCopyUtils; import org.springframework.util.StreamUtils; @@ -60,12 +61,11 @@ public OperationRequest convert(FilterableRequestSpecification requestSpec) { extractCookies(requestSpec)); } - private Collection extractCookies( + private Collection extractCookies( FilterableRequestSpecification requestSpec) { - Collection cookies = new ArrayList<>(); + Collection cookies = new ArrayList<>(); for (Cookie cookie : requestSpec.getCookies()) { - cookies.add( - new javax.servlet.http.Cookie(cookie.getName(), cookie.getValue())); + cookies.add(new RequestCookie(cookie.getName(), cookie.getValue())); } return cookies; } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 4617cb30d..906b8647b 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -25,8 +25,6 @@ import java.util.Collection; import java.util.Iterator; -import javax.servlet.http.Cookie; - import com.jayway.restassured.RestAssured; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.RequestSpecification; @@ -44,6 +42,7 @@ import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.RequestCookie; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -154,13 +153,13 @@ public void cookies() { .convert((FilterableRequestSpecification) requestSpec); assertThat(request.getCookies().size(), is(equalTo(2))); - Iterator cookieIterator = request.getCookies().iterator(); - Cookie cookie1 = cookieIterator.next(); + Iterator cookieIterator = request.getCookies().iterator(); + RequestCookie cookie1 = cookieIterator.next(); assertThat(cookie1.getName(), is(equalTo("cookie1"))); assertThat(cookie1.getValue(), is(equalTo("cookieVal1"))); - Cookie cookie2 = cookieIterator.next(); + RequestCookie cookie2 = cookieIterator.next(); assertThat(cookie2.getName(), is(equalTo("cookie2"))); assertThat(cookie2.getValue(), is(equalTo("cookieVal2"))); } From 31ad4b40a066d81c0526622b0cb9996ae68be399 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 3 Mar 2017 16:57:55 +0000 Subject: [PATCH 206/898] Upgrade the Grails sample to Grails 3.2.6 Closes gh-357 --- samples/rest-notes-grails/build.gradle | 43 ++++++++--------- samples/rest-notes-grails/gradle.properties | 4 +- .../grails-app/conf/application.yml | 2 +- .../com/example/IndexController.groovy | 48 ------------------- .../InternalServerErrorController.groovy | 28 ----------- .../com/example/NotFoundController.groovy | 28 ----------- .../{ => com/example}/UrlMappings.groovy | 16 +++++-- .../init/{ => com/example}/BootStrap.groovy | 2 +- .../grails-app/views/error.gson | 6 +++ .../grails-app/views/notFound.gson | 6 +++ .../src/docs/asciidoc/index.adoc | 24 ---------- .../com/example/ApiDocumentationSpec.groovy | 33 +------------ 12 files changed, 48 insertions(+), 192 deletions(-) delete mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy delete mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy delete mode 100644 samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy rename samples/rest-notes-grails/grails-app/controllers/{ => com/example}/UrlMappings.groovy (58%) rename samples/rest-notes-grails/grails-app/init/{ => com/example}/BootStrap.groovy (97%) create mode 100644 samples/rest-notes-grails/grails-app/views/error.gson create mode 100644 samples/rest-notes-grails/grails-app/views/notFound.gson diff --git a/samples/rest-notes-grails/build.gradle b/samples/rest-notes-grails/build.gradle index 64e2e6d8e..c0bce855d 100644 --- a/samples/rest-notes-grails/build.gradle +++ b/samples/rest-notes-grails/build.gradle @@ -1,36 +1,26 @@ buildscript { - ext { - grailsVersion = project.grailsVersion - } repositories { - mavenLocal() maven { url "https://repo.grails.org/grails/core" } - maven { url 'https://repo.spring.io/libs-snapshot' } } dependencies { classpath "org.grails:grails-gradle-plugin:$grailsVersion" - classpath "org.grails.plugins:hibernate:4.3.10.5" - classpath 'org.ajoberstar:gradle-git:1.1.0' + classpath "org.grails.plugins:hibernate5:6.0.7" } } plugins { - id "io.spring.dependency-management" version "0.5.4.RELEASE" id 'org.asciidoctor.convert' version '1.5.3' } version "0.1" group "com.example" -apply plugin: "spring-boot" apply plugin: "war" -apply plugin: 'eclipse' -apply plugin: 'idea' +apply plugin: "eclipse" +apply plugin: "idea" apply plugin: "org.grails.grails-web" ext { - grailsVersion = project.grailsVersion - gradleWrapperVersion = project.gradleWrapperVersion restDocsVersion = "1.2.0.BUILD-SNAPSHOT" snippetsDir = file('src/docs/generated-snippets') } @@ -43,12 +33,12 @@ repositories { dependencyManagement { dependencies { + dependency "org.springframework.restdocs:spring-restdocs-core:$restDocsVersion" dependency "org.springframework.restdocs:spring-restdocs-restassured:$restDocsVersion" dependency "org.springframework.restdocs:spring-restdocs-asciidoctor:$restDocsVersion" } imports { mavenBom "org.grails:grails-bom:$grailsVersion" - mavenBom "org.springframework:spring-framework-bom:4.3.1.RELEASE" } applyMavenExclusions false } @@ -57,11 +47,13 @@ dependencies { asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor" compile "org.springframework.boot:spring-boot-starter-logging" - compile "org.springframework.boot:spring-boot-starter-actuator" compile "org.springframework.boot:spring-boot-autoconfigure" + compile "org.springframework.boot:spring-boot-starter-actuator" compile "org.springframework.boot:spring-boot-starter-tomcat" + compile "org.grails:grails-core" compile "org.grails:grails-plugin-url-mappings" compile "org.grails:grails-plugin-rest" + compile "org.grails:grails-plugin-codecs" compile "org.grails:grails-plugin-interceptors" compile "org.grails:grails-plugin-services" compile "org.grails:grails-plugin-datasource" @@ -69,22 +61,25 @@ dependencies { compile "org.grails:grails-plugin-async" compile "org.grails:grails-web-boot" compile "org.grails:grails-logging" - - compile "org.grails.plugins:hibernate" compile "org.grails.plugins:cache" - compile "org.hibernate:hibernate-ehcache" + compile "org.grails.plugins:hibernate5" + compile "org.hibernate:hibernate-core:5.1.2.Final" + compile "org.hibernate:hibernate-ehcache:5.1.2.Final" + compile "org.grails.plugins:views-json" + compile "org.grails.plugins:views-json-templates" + console "org.grails:grails-console" + + profile "org.grails.profiles:rest-api" runtime "com.h2database:h2" testCompile "org.grails:grails-plugin-testing" testCompile "org.grails.plugins:geb" - testCompile 'org.springframework.restdocs:spring-restdocs-restassured' - - console "org.grails:grails-console" -} + testCompile "org.grails:grails-datastore-rest-client" + testCompile "org.springframework.restdocs:spring-restdocs-restassured" -task wrapper(type: Wrapper) { - gradleVersion = gradleWrapperVersion + testRuntime "org.seleniumhq.selenium:selenium-htmlunit-driver:2.47.1" + testRuntime "net.sourceforge.htmlunit:htmlunit:2.18" } ext { diff --git a/samples/rest-notes-grails/gradle.properties b/samples/rest-notes-grails/gradle.properties index 1b1c50f8e..d8bda96d7 100644 --- a/samples/rest-notes-grails/gradle.properties +++ b/samples/rest-notes-grails/gradle.properties @@ -1,2 +1,2 @@ -grailsVersion=3.0.15 -gradleWrapperVersion=2.3 +grailsVersion=3.2.6 +gradleWrapperVersion=3.4 \ No newline at end of file diff --git a/samples/rest-notes-grails/grails-app/conf/application.yml b/samples/rest-notes-grails/grails-app/conf/application.yml index 3fe5b2978..915c149a9 100644 --- a/samples/rest-notes-grails/grails-app/conf/application.yml +++ b/samples/rest-notes-grails/grails-app/conf/application.yml @@ -1,6 +1,6 @@ --- grails: - profile: web-api + profile: rest-api codegen: defaultPackage: com.example info: diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy deleted file mode 100644 index 01f585182..000000000 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/IndexController.groovy +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2014-2016 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 com.example - -import grails.core.GrailsApplication -import grails.util.Environment - -class IndexController { - - GrailsApplication grailsApplication - - def index() { - render(contentType: 'application/json') { - message = "Welcome to Grails!" - environment = Environment.current.name - appversion = grailsApplication.metadata['info.app.version'] - grailsversion = grailsApplication.metadata['info.app.grailsVersion'] - appprofile = grailsApplication.config.grails?.profile - groovyversion = GroovySystem.getVersion() - jvmversion = System.getProperty('java.version') - controllers = array { - for (c in grailsApplication.controllerClasses) { - controller([name: c.fullName]) - } - } - plugins = array { - for (p in grailsApplication.mainContext.pluginManager.allPlugins) { - plugin([name: p.fullName]) - } - } - } - } - -} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy deleted file mode 100644 index c0e80312c..000000000 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/InternalServerErrorController.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014-2016 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 com.example - -class InternalServerErrorController { - - def index() { - render(contentType: 'application/json') { - error = 500 - message = "Internal server error" - } - } - -} diff --git a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy deleted file mode 100644 index 039968a93..000000000 --- a/samples/rest-notes-grails/grails-app/controllers/com/example/NotFoundController.groovy +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2014-2016 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 com.example - -class NotFoundController { - - def index() { - render(contentType: 'application/json') { - error = 404 - message = "Not Found" - } - } - -} diff --git a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy b/samples/rest-notes-grails/grails-app/controllers/com/example/UrlMappings.groovy similarity index 58% rename from samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy rename to samples/rest-notes-grails/grails-app/controllers/com/example/UrlMappings.groovy index 7d8cffe56..190e41e39 100644 --- a/samples/rest-notes-grails/grails-app/controllers/UrlMappings.groovy +++ b/samples/rest-notes-grails/grails-app/controllers/com/example/UrlMappings.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -14,12 +14,20 @@ * limitations under the License. */ +package com.example + class UrlMappings { static mappings = { - "/"(controller: 'index') - "500"(controller: 'InternalServerError') - "404"(controller: 'NotFound') + delete "/$controller/$id(.$format)?"(action:"delete") + get "/$controller(.$format)?"(action:"index") + get "/$controller/$id(.$format)?"(action:"show") + post "/$controller(.$format)?"(action:"save") + put "/$controller/$id(.$format)?"(action:"update") + patch "/$controller/$id(.$format)?"(action:"patch") + + "500"(view: '/error') + "404"(view: '/notFound') } } diff --git a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy b/samples/rest-notes-grails/grails-app/init/com/example/BootStrap.groovy similarity index 97% rename from samples/rest-notes-grails/grails-app/init/BootStrap.groovy rename to samples/rest-notes-grails/grails-app/init/com/example/BootStrap.groovy index 0efbc954f..92ec4c144 100644 --- a/samples/rest-notes-grails/grails-app/init/BootStrap.groovy +++ b/samples/rest-notes-grails/grails-app/init/com/example/BootStrap.groovy @@ -14,7 +14,7 @@ * limitations under the License. */ -import com.example.Note +package com.example class BootStrap { diff --git a/samples/rest-notes-grails/grails-app/views/error.gson b/samples/rest-notes-grails/grails-app/views/error.gson new file mode 100644 index 000000000..314e80db7 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/views/error.gson @@ -0,0 +1,6 @@ +response.status 500 + +json { + message "Internal server error" + error 500 +} \ No newline at end of file diff --git a/samples/rest-notes-grails/grails-app/views/notFound.gson b/samples/rest-notes-grails/grails-app/views/notFound.gson new file mode 100644 index 000000000..1a710b267 --- /dev/null +++ b/samples/rest-notes-grails/grails-app/views/notFound.gson @@ -0,0 +1,6 @@ +response.status 404 + +json { + message "Not Found" + error 404 +} \ No newline at end of file diff --git a/samples/rest-notes-grails/src/docs/asciidoc/index.adoc b/samples/rest-notes-grails/src/docs/asciidoc/index.adoc index 231af2ecf..c1dd35966 100644 --- a/samples/rest-notes-grails/src/docs/asciidoc/index.adoc +++ b/samples/rest-notes-grails/src/docs/asciidoc/index.adoc @@ -62,30 +62,6 @@ use of HTTP status codes. = Resources -[[resources-index]] -== Index - -The index provides the entry point into the service. - - - -[[resources-index-access]] -=== Accessing the index - -A `GET` request is used to access the index - -==== Example request - -include::{snippets}/index-example/curl-request.adoc[] - -==== Response structure - -include::{snippets}/index-example/response-fields.adoc[] - -==== Example response - -include::{snippets}/index-example/http-response.adoc[] - [[resources-notes]] == Notes diff --git a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy index 102426b96..2e15d73bc 100644 --- a/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy +++ b/samples/rest-notes-grails/src/integration-test/groovy/com/example/ApiDocumentationSpec.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -59,34 +59,6 @@ class ApiDocumentationSpec extends Specification { .build() } - void 'test and document get request for /index'() { - expect: - given(this.documentationSpec) - .accept(MediaType.APPLICATION_JSON.toString()) - .filter(document('index-example', - preprocessRequest(modifyUris() - .host('api.example.com') - .removePort()), - preprocessResponse(prettyPrint()), - responseFields( - fieldWithPath('message').description('Welcome to Grails!'), - fieldWithPath('environment').description("The running environment"), - fieldWithPath('appversion').description('version of the app that is running'), - fieldWithPath('grailsversion').description('the version of grails used in this project'), - fieldWithPath('appprofile').description('the profile of grails used in this project'), - fieldWithPath('groovyversion').description('the version of groovy used in this project'), - fieldWithPath('jvmversion').description('the version of the jvm used in this project'), - subsectionWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'), - subsectionWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'), - ))) - .when() - .port(this.serverPort) - .get('/') - .then() - .assertThat() - .statusCode(is(200)) - } - void 'test and document notes list request'() { expect: given(this.documentationSpec) @@ -97,7 +69,6 @@ class ApiDocumentationSpec extends Specification { .removePort()), preprocessResponse(prettyPrint()), responseFields( - fieldWithPath('[].class').description('the class of the resource'), fieldWithPath('[].id').description('the id of the note'), fieldWithPath('[].title').description('the title of the note'), fieldWithPath('[].body').description('the body of the note'), @@ -127,7 +98,6 @@ class ApiDocumentationSpec extends Specification { subsectionWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note') ), responseFields( - fieldWithPath('class').description('the class of the resource'), fieldWithPath('id').description('the id of the note'), fieldWithPath('title').description('the title of the note'), fieldWithPath('body').description('the body of the note'), @@ -152,7 +122,6 @@ class ApiDocumentationSpec extends Specification { .removePort()), preprocessResponse(prettyPrint()), responseFields( - fieldWithPath('class').description('the class of the resource'), fieldWithPath('id').description('the id of the note'), fieldWithPath('title').description('the title of the note'), fieldWithPath('body').description('the body of the note'), From b6f986556e8ab19ccfdb984381687b294ba62ea1 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 3 Mar 2017 21:03:39 +0000 Subject: [PATCH 207/898] Upgrade to Gradle 3.4 Closes gh-283 --- build.gradle | 8 ++++++-- .../build/SampleBuildConfigurer.groovy | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53639 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index b4f341154..b18f28f6e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,11 +5,15 @@ buildscript { maven { url 'https://plugins.gradle.org/m2/' } } dependencies { - classpath 'io.spring.gradle:dependency-management-plugin:0.5.5.RELEASE' classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.7' - classpath 'io.spring.gradle:spring-io-plugin:0.0.5.RELEASE' + classpath 'io.spring.gradle:spring-io-plugin:0.0.6.RELEASE' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:1.2' } + configurations.classpath.resolutionStrategy.eachDependency { + if (it.requested.name == 'dependency-management-plugin') { + it.useVersion '1.0.0.RELEASE' + } + } } allprojects { diff --git a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy index 69b6f1fd5..5ef87faed 100644 --- a/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy +++ b/buildSrc/src/main/groovy/org/springframework/restdocs/build/SampleBuildConfigurer.groovy @@ -121,7 +121,7 @@ public class SampleBuildConfigurer { private Task createVerifyIncludes(Project project, File buildDir) { Task verifyIncludesTask = project.tasks.create("${name}VerifyIncludes") verifyIncludesTask.description = "Verifies the includes in the ${name} sample" - verifyIncludesTask << { + verifyIncludesTask.doLast { Map unprocessedIncludes = [:] buildDir.eachFileRecurse { file -> if (file.name.endsWith('.html')) { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5ccda13e9cb94678ba179b32452cf3d60dc36353..2c6137b87896c8f70315ae454e00a969ef5f6019 100644 GIT binary patch delta 1762 zcmY*Z3rv$&6u$inv`UK;1cg>AAP<3I*QyZ#iC_c)5wQ50V{{aR$v}Zv1viU2n4rAw zHXk9|7`Qrh0WIp7z(AnoQJ^@Taf|a2Ky)&#+2S6eyZ^ZaY?J0Y_xrzd&i9{t+M-%+ zaV=LE7tOVri4dQUq%m2QLN7jn$jkc8K9xaR9n3lA91fb6coNBJH!cfCAAsjl7O*ep z9*a6VCYJ%?kktbqvaIWX&^huQY=H5zyG0q^Y^gOcE1W7Q(?4$`4;Zfn8yz6nFBecv z*>WdaV6@@SXF^aDdz%(4Oytq@(oKncK5-G5byoW!9(y<9ji>AU6QoPxr45a;WtU`2 z6gV_lHe()9e0DOx*@W|xJ@zjxZ^`PA3J$4Tqh=RYi36P*^Zepe8K#S-S>rwp3&X39 zuKZ}+>)vk3-r#Ei%4f$sxB9LaS)HujDXe^7zUybEDXb?bcx~Y`;brDnieS8Bhu^@# zi)Z9XTNK{gM>K{StzFB8klihJ?`O`x`sU5gV-}8QjAZ)j*LUVPyIrqWC5`6yt(%p0 z#!9U_neDrDxGwN_=a*k;wk^K$kGyU~?NHyU+9nJB^N}>+YkRTL^G?swiAc@;FTQL~ z`1XawRDG*RRQ%WZ;oFL92X>j6^@g&SuiX}TQM^~_&n2ikt^9;x11wiP1VWPf3J9HB z`a>EBcVG@Ys?C(}A?V7Ja3Of04x)i)!B5t}{HOVsivK=vg9nVMWQa0#N6s>K?2tb` z)i`&%Jwke4EG<}opXS-<4wkF!K|N7prd`c-cWH24d&vqO9X-dT&2arw`l#r_JGAtu zZWYz|es7}8M3aJQ6wR2+XS+6(y0oqhaBl8O1e~L%byfNlIQQyfrgz!Zu=cgJ-DwD62Zb99BF+ccXmEwoxIx5J zE3tII8JmOq(M($4;qUt9gR}lV5%c%} zu0H3E1x8q5>}C`(ohA5AN$}LL4-@M65lHSf${=xqP;1Hw<%16o(kqGY7cu46L2-sK*z`-)^Mgj{S93bIJ-#)}7{ zz{0)(5mR`Mcn_F*_e*UJxyMPrGh_uUZ=|?>s-Jk!o!-izh{?Y|XfYO)&SGB{JckcC zjXol?+ecbkuF)?#sBv@9N5XoObLlMC-@c~YRNFxkX96ALjV35h+ zD2{+Zvr%sKpq9kbB<)Nun7`{umQR(Dsi}T|C`9JO>Vw(zJA~TI_KVuYjpZG z+B8T*o6JW@BtrITb&jc0L_i%~`zkKSYp2zVgy#u7G$%19lCotq1Dz`XUaAwwT(i>w5|IGYWyjL<^G2gcLpdzR^1yh8|#Qoh3q7N^|BtmgcB zn+3p>`n{YFi{dRqY{1k|A!|SPd8kN4s!)f^PcFq{d;J&2YXXB+l|ib?8aGv?n@14# ziEx`o6GiTzhieZ`j&L~To$VXfBp0Vmy}5Wp^hl6PU;14cSf?F4LOr=2!c)lmPR{1u zDu|oX7Zv@Lr+RI)lv?8i#nYqH7K;7@PqaF;TsM|BDF|A<&pCZVYww=A@fnfdZ+xlzjFDU^>CNsOu?nmF*6<(c_Rciezti0&#Gq>uXKk((<6E5o#Z*5wiMSJ#WJQ>MRNPjTyoj+O%YOZ#EY@Y zxE8V(YIpUNlAf;92(9O6CQ~5$Pev)squVHg(uq1!|U1A7>LvfxWxfaC^-+{d|q|wvzPb&IvbN3|`e$ z%T+-d9<_*OKk7`6oR^AY8r5N5$y(?44abxtArU4B*)KrIi(@cgRd)as_f5BiN+~D3 ze)#SWRk(?6uIMXX&PSPF)48_qzEw&>=iDo+C#Q(aQ2$x`Orv#GZ_eiJ# zJv27Z;|K?akyk!5&^N@pf#a28S+5#w2YV&d^gVVS_br&S2D*dL{ Date: Sat, 4 Mar 2017 20:40:42 +0000 Subject: [PATCH 208/898] Remove use of Spring Boot from REST Assured module's integration tests Closes gh-360 --- spring-restdocs-restassured/build.gradle | 5 +- .../RestAssuredRequestConverterTests.java | 86 +++++--------- ...uredRestDocumentationIntegrationTests.java | 108 ++++++------------ .../restdocs/restassured/TomcatServer.java | 107 +++++++++++++++++ 4 files changed, 171 insertions(+), 135 deletions(-) create mode 100644 spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java diff --git a/spring-restdocs-restassured/build.gradle b/spring-restdocs-restassured/build.gradle index a861c7e51..245a697ba 100644 --- a/spring-restdocs-restassured/build.gradle +++ b/spring-restdocs-restassured/build.gradle @@ -9,13 +9,10 @@ dependencies { compile project(':spring-restdocs-core') compile 'com.jayway.restassured:rest-assured' + testCompile 'org.apache.tomcat.embed:tomcat-embed-core:8.5.11' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-library' - testCompile 'org.springframework.hateoas:spring-hateoas' - testCompile 'org.springframework.boot:spring-boot-starter-web:1.3.6.RELEASE' - testCompile 'org.springframework:spring-test' testCompile project(path: ':spring-restdocs-core', configuration: 'testArtifacts') - testRuntime 'commons-logging:commons-logging:1.2' } test { diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java index 9c4f7615e..9b04d0786 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRequestConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -28,25 +28,15 @@ import com.jayway.restassured.RestAssured; import com.jayway.restassured.specification.FilterableRequestSpecification; import com.jayway.restassured.specification.RequestSpecification; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.IntegrationTest; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.restassured.RestAssuredRequestConverterTests.TestApplication; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -57,33 +47,29 @@ * * @author Andy Wilkinson */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = TestApplication.class) -@WebAppConfiguration -@IntegrationTest("server.port=0") public class RestAssuredRequestConverterTests { + @ClassRule + public static TomcatServer tomcat = new TomcatServer(); + @Rule public final ExpectedException thrown = ExpectedException.none(); private final RestAssuredRequestConverter factory = new RestAssuredRequestConverter(); - @Value("${local.server.port}") - private int port; - @Test public void requestUri() { - RequestSpecification requestSpec = RestAssured.given().port(this.port); + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.get("/foo/bar"); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); - assertThat(request.getUri(), - is(equalTo(URI.create("http://localhost:" + this.port + "/foo/bar")))); + assertThat(request.getUri(), is(equalTo( + URI.create("http://localhost:" + tomcat.getPort() + "/foo/bar")))); } @Test public void requestMethod() { - RequestSpecification requestSpec = RestAssured.given().port(this.port); + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.head("/foo/bar"); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -92,7 +78,7 @@ public void requestMethod() { @Test public void queryStringParameters() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .queryParam("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory @@ -103,7 +89,7 @@ public void queryStringParameters() { @Test public void queryStringFromUrlParameters() { - RequestSpecification requestSpec = RestAssured.given().port(this.port); + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()); requestSpec.get("/?foo=bar"); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -113,7 +99,7 @@ public void queryStringFromUrlParameters() { @Test public void formParameters() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .formParameter("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory @@ -124,7 +110,7 @@ public void formParameters() { @Test public void requestParameters() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .parameter("foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory @@ -135,7 +121,7 @@ public void requestParameters() { @Test public void headers() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .header("Foo", "bar"); requestSpec.get("/"); OperationRequest request = this.factory @@ -144,15 +130,15 @@ public void headers() { assertThat(request.getHeaders().get("Foo"), is(equalTo(Arrays.asList("bar")))); assertThat(request.getHeaders().get("Accept"), is(equalTo(Arrays.asList("*/*")))); assertThat(request.getHeaders().get("Host"), - is(equalTo(Arrays.asList("localhost:" + this.port)))); + is(equalTo(Arrays.asList("localhost:" + tomcat.getPort())))); } @Test public void multipart() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("a", "a.txt", "alpha", null) .multiPart("b", new ObjectBody("bar"), "application/json"); - requestSpec.post().then().statusCode(200); + requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); Collection parts = request.getParts(); @@ -174,7 +160,7 @@ public void multipart() { @Test public void byteArrayBody() { RequestSpecification requestSpec = RestAssured.given().body("body".getBytes()) - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); this.factory.convert((FilterableRequestSpecification) requestSpec); } @@ -182,7 +168,7 @@ public void byteArrayBody() { @Test public void stringBody() { RequestSpecification requestSpec = RestAssured.given().body("body") - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -192,7 +178,7 @@ public void stringBody() { @Test public void objectBody() { RequestSpecification requestSpec = RestAssured.given().body(new ObjectBody("bar")) - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -203,7 +189,7 @@ public void objectBody() { public void byteArrayInputStreamBody() { RequestSpecification requestSpec = RestAssured.given() .body(new ByteArrayInputStream(new byte[] { 1, 2, 3, 4 })) - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -213,7 +199,7 @@ public void byteArrayInputStreamBody() { @Test public void fileBody() { RequestSpecification requestSpec = RestAssured.given() - .body(new File("src/test/resources/body.txt")).port(this.port); + .body(new File("src/test/resources/body.txt")).port(tomcat.getPort()); requestSpec.post(); OperationRequest request = this.factory .convert((FilterableRequestSpecification) requestSpec); @@ -224,7 +210,7 @@ public void fileBody() { public void fileInputStreamBody() throws FileNotFoundException { FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); RequestSpecification requestSpec = RestAssured.given().body(inputStream) - .port(this.port); + .port(tomcat.getPort()); requestSpec.post(); this.thrown.expect(IllegalStateException.class); this.thrown.expectMessage("Cannot read content from input stream " + inputStream @@ -234,7 +220,7 @@ public void fileInputStreamBody() throws FileNotFoundException { @Test public void multipartWithByteArrayInputStreamBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("foo", "foo.txt", new ByteArrayInputStream("foo".getBytes())); requestSpec.post(); OperationRequest request = this.factory @@ -245,7 +231,7 @@ public void multipartWithByteArrayInputStreamBody() { @Test public void multipartWithStringBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("control", "foo"); requestSpec.post(); OperationRequest request = this.factory @@ -256,7 +242,7 @@ public void multipartWithStringBody() { @Test public void multipartWithByteArrayBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("control", "file", "foo".getBytes()); requestSpec.post(); OperationRequest request = this.factory @@ -267,7 +253,7 @@ public void multipartWithByteArrayBody() { @Test public void multipartWithFileBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart(new File("src/test/resources/body.txt")); requestSpec.post(); OperationRequest request = this.factory @@ -279,7 +265,7 @@ public void multipartWithFileBody() { @Test public void multipartWithFileInputStreamBody() throws FileNotFoundException { FileInputStream inputStream = new FileInputStream("src/test/resources/body.txt"); - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("foo", "foo.txt", inputStream); requestSpec.post(); this.thrown.expect(IllegalStateException.class); @@ -290,7 +276,7 @@ public void multipartWithFileInputStreamBody() throws FileNotFoundException { @Test public void multipartWithObjectBody() { - RequestSpecification requestSpec = RestAssured.given().port(this.port) + RequestSpecification requestSpec = RestAssured.given().port(tomcat.getPort()) .multiPart("control", new ObjectBody("bar")); requestSpec.post(); OperationRequest request = this.factory @@ -299,20 +285,6 @@ public void multipartWithObjectBody() { is(equalTo("{\"foo\":\"bar\"}"))); } - /** - * Minimal test web application. - */ - @Configuration - @EnableAutoConfiguration - @RestController - static class TestApplication { - - @RequestMapping("/") - void handle() { - } - - } - /** * Sample object body to verify JSON serialization. */ diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java index f81c91f39..4d1c4072f 100644 --- a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/RestAssuredRestDocumentationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2016 the original author or authors. + * Copyright 2014-2017 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. @@ -19,33 +19,19 @@ import java.io.File; import java.net.URL; import java.net.URLClassLoader; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import java.util.regex.Pattern; import com.jayway.restassured.builder.RequestSpecBuilder; import com.jayway.restassured.specification.RequestSpecification; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.IntegrationTest; -import org.springframework.boot.test.SpringApplicationConfiguration; -import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; -import org.springframework.restdocs.restassured.RestAssuredRestDocumentationIntegrationTests.TestApplication; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; import static com.jayway.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.equalTo; @@ -84,22 +70,19 @@ * * @author Andy Wilkinson */ -@RunWith(SpringJUnit4ClassRunner.class) -@SpringApplicationConfiguration(classes = TestApplication.class) -@WebAppConfiguration -@IntegrationTest("server.port=0") public class RestAssuredRestDocumentationIntegrationTests { @Rule public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation( "build/generated-snippets"); - @Value("${local.server.port}") - private int port; + @ClassRule + public static TomcatServer tomcat = new TomcatServer(); @Test public void defaultSnippetGeneration() { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("default")).get("/").then().statusCode(200); assertExpectedSnippetFilesExist(new File("build/generated-snippets/default"), "http-request.adoc", "http-response.adoc", "curl-request.adoc"); @@ -108,7 +91,8 @@ public void defaultSnippetGeneration() { @Test public void curlSnippetWithContent() throws Exception { String contentType = "text/plain; charset=UTF-8"; - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("curl-snippet-with-content")).accept("application/json") .content("content").contentType(contentType).post("/").then() .statusCode(200); @@ -117,7 +101,7 @@ public void curlSnippetWithContent() throws Exception { new File( "build/generated-snippets/curl-snippet-with-content/curl-request.adoc"), is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") - .content("$ curl 'http://localhost:" + this.port + "/' -i " + .content("$ curl 'http://localhost:" + tomcat.getPort() + "/' -i " + "-X POST -H 'Accept: application/json' " + "-H 'Content-Type: " + contentType + "' " + "-d 'content'")))); @@ -125,7 +109,8 @@ public void curlSnippetWithContent() throws Exception { @Test public void curlSnippetWithQueryStringOnPost() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("curl-snippet-with-query-string")) .accept("application/json").param("foo", "bar").param("a", "alpha") .post("/?foo=bar").then().statusCode(200); @@ -134,7 +119,7 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { new File( "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), is(snippet(asciidoctor()).withContents(codeBlock(asciidoctor(), "bash") - .content("$ curl " + "'http://localhost:" + this.port + .content("$ curl " + "'http://localhost:" + tomcat.getPort() + "/?foo=bar' -i -X POST " + "-H 'Accept: application/json' " + "-H 'Content-Type: " + contentType + "' " + "-d 'a=alpha'")))); @@ -142,7 +127,8 @@ public void curlSnippetWithQueryStringOnPost() throws Exception { @Test public void linksSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("links", links(linkWithRel("rel").description("The description")))) .accept("application/json").get("/").then().statusCode(200); @@ -153,7 +139,8 @@ public void linksSnippet() throws Exception { @Test public void pathParametersSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("path-parameters", pathParameters( parameterWithName("foo").description("The description")))) @@ -165,7 +152,8 @@ public void pathParametersSnippet() throws Exception { @Test public void requestParametersSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("request-parameters", requestParameters( parameterWithName("foo").description("The description")))) @@ -179,7 +167,8 @@ public void requestParametersSnippet() throws Exception { @Test public void requestFieldsSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("request-fields", requestFields(fieldWithPath("a").description("The description")))) .accept("application/json").content("{\"a\":\"alpha\"}").post("/").then() @@ -191,7 +180,8 @@ public void requestFieldsSnippet() throws Exception { @Test public void requestPartsSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("request-parts", requestParts(partWithName("a").description("The description")))) .multiPart("a", "foo").post("/upload").then().statusCode(200); @@ -202,7 +192,8 @@ public void requestPartsSnippet() throws Exception { @Test public void responseFieldsSnippet() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("response-fields", responseFields(fieldWithPath("a").description("The description"), fieldWithPath("links") @@ -216,7 +207,8 @@ public void responseFieldsSnippet() throws Exception { @Test public void parameterizedOutputDirectory() throws Exception { - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("{method-name}")).get("/").then().statusCode(200); assertExpectedSnippetFilesExist( new File("build/generated-snippets/parameterized-output-directory"), @@ -225,7 +217,7 @@ public void parameterizedOutputDirectory() throws Exception { @Test public void multiStep() throws Exception { - RequestSpecification spec = new RequestSpecBuilder().setPort(this.port) + RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) .addFilter(documentationConfiguration(this.restDocumentation)) .addFilter(document("{method-name}-{step}")).build(); given(spec).get("/").then().statusCode(200); @@ -245,7 +237,7 @@ public void multiStep() throws Exception { @Test public void additionalSnippets() throws Exception { RestDocumentationFilter documentation = document("{method-name}-{step}"); - RequestSpecification spec = new RequestSpecBuilder().setPort(this.port) + RequestSpecification spec = new RequestSpecBuilder().setPort(tomcat.getPort()) .addFilter(documentationConfiguration(this.restDocumentation)) .addFilter(documentation).build(); given(spec) @@ -262,7 +254,8 @@ public void additionalSnippets() throws Exception { @Test public void preprocessedRequest() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .header("a", "alpha").header("b", "bravo").contentType("application/json") .accept("application/json").content("{\"a\":\"alpha\"}") .filter(document("original-request")) @@ -279,7 +272,7 @@ public void preprocessedRequest() throws Exception { .header("a", "alpha").header("b", "bravo") .header("Accept", MediaType.APPLICATION_JSON_VALUE) .header("Content-Type", "application/json; charset=UTF-8") - .header("Host", "localhost:" + this.port) + .header("Host", "localhost:" + tomcat.getPort()) .header("Content-Length", "13") .content("{\"a\":\"alpha\"}")))); String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); @@ -297,7 +290,8 @@ public void preprocessedRequest() throws Exception { @Test public void preprocessedResponse() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); - given().port(this.port).filter(documentationConfiguration(this.restDocumentation)) + given().port(tomcat.getPort()) + .filter(documentationConfiguration(this.restDocumentation)) .filter(document("original-response")) .filter(document("preprocessed-response", preprocessResponse( prettyPrint(), maskLinks(), @@ -327,7 +321,7 @@ public void customSnippetTemplate() throws Exception { ClassLoader previous = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(classLoader); try { - given().port(this.port).accept("application/json") + given().port(tomcat.getPort()).accept("application/json") .filter(documentationConfiguration(this.restDocumentation)) .filter(document("custom-snippet-template")).get("/").then() .statusCode(200); @@ -348,38 +342,4 @@ private void assertExpectedSnippetFilesExist(File directory, String... snippets) } } - /** - * Minimal test application called by the tests. - */ - @Configuration - @EnableAutoConfiguration - @RestController - static class TestApplication { - - @RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity> foo() { - Map response = new HashMap<>(); - response.put("a", "alpha"); - Map link = new HashMap<>(); - link.put("rel", "rel"); - link.put("href", "href"); - response.put("links", Arrays.asList(link)); - HttpHeaders headers = new HttpHeaders(); - headers.add("a", "alpha"); - headers.add("Foo", "http://localhost:12345/foo/bar"); - return new ResponseEntity<>(response, headers, HttpStatus.OK); - } - - @RequestMapping(value = "/company/5", produces = MediaType.APPLICATION_JSON_VALUE) - public String bar() { - return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}"; - } - - @RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public void upload() { - - } - - } - } diff --git a/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java new file mode 100644 index 000000000..75ee97663 --- /dev/null +++ b/spring-restdocs-restassured/src/test/java/org/springframework/restdocs/restassured/TomcatServer.java @@ -0,0 +1,107 @@ +/* + * Copyright 2014-2017 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.restassured; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; +import org.junit.rules.ExternalResource; + +/** + * {@link ExternalResource} that starts and stops a Tomcat server. + * + * @author Andy Wilkinson + */ +class TomcatServer extends ExternalResource { + + private Tomcat tomcat; + + private int port; + + @Override + protected void before() throws LifecycleException { + this.tomcat = new Tomcat(); + this.tomcat.getConnector().setPort(0); + Context context = this.tomcat.addContext("/", null); + this.tomcat.addServlet("/", "test", new TestServlet()); + context.addServletMappingDecoded("/", "test"); + this.tomcat.start(); + this.port = this.tomcat.getConnector().getLocalPort(); + } + + @Override + protected void after() { + try { + this.tomcat.stop(); + } + catch (LifecycleException ex) { + throw new RuntimeException(ex); + } + } + + int getPort() { + return this.port; + } + + /** + * {@link HttpServlet} used to handle requests in the tests. + */ + private static final class TestServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + respondWithJson(response); + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + respondWithJson(response); + } + + private void respondWithJson(HttpServletResponse response) + throws IOException, JsonProcessingException { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json"); + Map content = new HashMap<>(); + content.put("a", "alpha"); + Map link = new HashMap<>(); + link.put("rel", "rel"); + link.put("href", "href"); + content.put("links", Arrays.asList(link)); + response.getWriter().println(new ObjectMapper().writeValueAsString(content)); + response.setHeader("a", "alpha"); + response.setHeader("Foo", "http://localhost:12345/foo/bar"); + response.flushBuffer(); + } + + } + +} From 727f6ccd78a63870d643d3ae49b50ffeb8b4d940 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 4 Mar 2017 20:45:47 +0000 Subject: [PATCH 209/898] Protect the tests against changes to MockMvc's HTTP header ordering Closes gh-359 --- ...kMvcRestDocumentationIntegrationTests.java | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index 2b2df2e8e..edc4c3331 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -21,6 +21,7 @@ import java.net.URLClassLoader; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -39,10 +40,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationIntegrationTests.TestConfiguration; +import org.springframework.restdocs.test.SnippetMatchers.HttpRequestMatcher; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.util.FileSystemUtils; import org.springframework.web.bind.annotation.RequestMapping; @@ -60,6 +63,7 @@ import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.links; +import static org.springframework.restdocs.mockmvc.IterableEnumeration.iterable; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; @@ -317,8 +321,9 @@ public void responseFieldsSnippet() throws Exception { mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andDo(document("links", responseFields( - fieldWithPath("a").description("The description"), + .andDo(document("links", + responseFields(fieldWithPath("a") + .description("The description"), fieldWithPath("links").description("Links to other resources")))); assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"), @@ -388,38 +393,44 @@ public void preprocessedRequest() throws Exception { Pattern pattern = Pattern.compile("(\"alpha\")"); - mockMvc.perform(get("/").header("a", "alpha").header("b", "bravo") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) + MvcResult result = mockMvc + .perform(get("/").header("a", "alpha").header("b", "bravo") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON).content("{\"a\":\"alpha\"}")) .andExpect(status().isOk()).andDo(document("original-request")) .andDo(document("preprocessed-request", preprocessRequest(prettyPrint(), removeHeaders("a", HttpHeaders.HOST, HttpHeaders.CONTENT_LENGTH), - replacePattern(pattern, "\"<>\"")))); + replacePattern(pattern, "\"<>\"")))) + .andReturn(); + HttpRequestMatcher originalRequest = httpRequest(asciidoctor(), RequestMethod.GET, + "/"); + for (String headerName : iterable(result.getRequest().getHeaderNames())) { + originalRequest.header(headerName, result.getRequest().getHeader(headerName)); + } assertThat( new File("build/generated-snippets/original-request/http-request.adoc"), - is(snippet(asciidoctor()) - .withContents( - httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("a", "alpha").header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", - MediaType.APPLICATION_JSON_VALUE) - .header("Host", "localhost:8080") - .header("Content-Length", "13") - .content("{\"a\":\"alpha\"}")))); + is(snippet(asciidoctor()).withContents(originalRequest + .header("Host", "localhost:8080").header("Content-Length", "13") + .content("{\"a\":\"alpha\"}")))); + HttpRequestMatcher preprocessedRequest = httpRequest(asciidoctor(), + RequestMethod.GET, "/"); + List removedHeaders = Arrays.asList("a", HttpHeaders.HOST, + HttpHeaders.CONTENT_LENGTH); + for (String headerName : iterable(result.getRequest().getHeaderNames())) { + if (!removedHeaders.contains(headerName)) { + preprocessedRequest.header(headerName, + result.getRequest().getHeader(headerName)); + } + } String prettyPrinted = String.format("{%n \"a\" : \"<>\"%n}"); assertThat( new File( "build/generated-snippets/preprocessed-request/http-request.adoc"), is(snippet(asciidoctor()) - .withContents(httpRequest(asciidoctor(), RequestMethod.GET, "/") - .header("b", "bravo") - .header("Content-Type", "application/json") - .header("Accept", MediaType.APPLICATION_JSON_VALUE) - .content(prettyPrinted)))); + .withContents(preprocessedRequest.content(prettyPrinted)))); } @Test From 3a0f4ab68e0bc23e18212c38c169e8f5f0492904 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 4 Mar 2017 21:43:12 +0000 Subject: [PATCH 210/898] Fix the Slate sample's tests Closes gh-339 --- samples/rest-notes-slate/build.gradle | 1 - .../com/example/notes/ApiDocumentation.java | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/samples/rest-notes-slate/build.gradle b/samples/rest-notes-slate/build.gradle index d65d9d483..2911c2884 100644 --- a/samples/rest-notes-slate/build.gradle +++ b/samples/rest-notes-slate/build.gradle @@ -27,7 +27,6 @@ ext { } ext['spring-restdocs.version'] = '1.2.0.BUILD-SNAPSHOT' -ext['spring.version']='4.3.1.RELEASE' dependencies { compile 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java index 073af2bcd..bc611323d 100644 --- a/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-slate/src/test/java/com/example/notes/ApiDocumentation.java @@ -28,6 +28,7 @@ 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.payload.PayloadDocumentation.subsectionWithPath; import static org.springframework.restdocs.templates.TemplateFormats.markdown; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -116,7 +117,7 @@ public void indexExample() throws Exception { linkWithRel("tags").description("The [Tags](#tags) resource"), linkWithRel("profile").description("The ALPS profile for the service")), responseFields( - fieldWithPath("_links").description("Links to other resources")))); + subsectionWithPath("_links").description("Links to other resources")))); } @@ -134,7 +135,8 @@ public void notesListExample() throws Exception { .andExpect(status().isOk()) .andDo(document("notes-list-example", responseFields( - fieldWithPath("_embedded.notes").description("An array of [Note](#note) resources")))); + subsectionWithPath("_embedded.notes").description("An array of [Note](#note) resources"), + subsectionWithPath("_links").description("Links to other resources")))); } @Test @@ -197,12 +199,13 @@ public void noteGetExample() throws Exception { .andExpect(jsonPath("_links.tags", is(notNullValue()))) .andDo(document("note-get-example", links( - linkWithRel("self").description("This note"), + linkWithRel("self").description("Canonical link for this resource"), + linkWithRel("note").description("This note"), 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("Links to other resources")))); + subsectionWithPath("_links").description("Links to other resources")))); } @Test @@ -218,7 +221,8 @@ public void tagsListExample() throws Exception { .andExpect(status().isOk()) .andDo(document("tags-list-example", responseFields( - fieldWithPath("_embedded.tags").description("An array of [Tag](#tag) resources")))); + subsectionWithPath("_embedded.tags").description("An array of [Tag](#tag) resources"), + subsectionWithPath("_links").description("Links to other resources")))); } @Test @@ -295,11 +299,12 @@ public void tagGetExample() throws Exception { .andExpect(jsonPath("name", is(tag.get("name")))) .andDo(document("tag-get-example", links( - linkWithRel("self").description("This tag"), + linkWithRel("self").description("Canonical link for this resource"), + linkWithRel("tag").description("This tag"), linkWithRel("notes").description("The notes that have this tag")), responseFields( fieldWithPath("name").description("The name of the tag"), - fieldWithPath("_links").description("Links to other resources")))); + subsectionWithPath("_links").description("Links to other resources")))); } @Test From ca86e3ad8c874fce6211876daf3ce7db9b9f1ace Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Sat, 4 Mar 2017 22:12:09 +0000 Subject: [PATCH 211/898] Upgrade Slate sample to Slate 1.5 Closes gh-361 --- samples/rest-notes-slate/slate/.gitignore | 4 +- samples/rest-notes-slate/slate/CHANGELOG.md | 50 +- samples/rest-notes-slate/slate/Gemfile | 15 +- samples/rest-notes-slate/slate/Gemfile.lock | 194 +- samples/rest-notes-slate/slate/README.md | 119 +- samples/rest-notes-slate/slate/Rakefile | 6 - samples/rest-notes-slate/slate/Vagrantfile | 39 + samples/rest-notes-slate/slate/config.rb | 11 + samples/rest-notes-slate/slate/deploy.sh | 203 + .../rest-notes-slate/slate/lib/multilang.rb | 12 + ...api-guide.md.erb => api-guide.html.md.erb} | 0 .../slate/source/images/logo.png | Bin 3507 -> 22317 bytes .../slate/source/includes/_errors.md | 20 + .../slate/source/index.html.md | 189 + .../slate/source/javascripts/app/_lang.js | 8 +- .../slate/source/javascripts/app/_search.js | 1 + .../slate/source/javascripts/app/_toc.js | 2 + .../slate/source/javascripts/lib/_jquery.js | 9831 +++++++++++++++++ .../slate/source/layouts/layout.erb | 20 +- .../{_normalize.css => _normalize.scss} | 0 .../slate/source/stylesheets/_syntax.scss.erb | 27 - .../slate/source/stylesheets/_variables.scss | 84 +- .../slate/source/stylesheets/print.css.scss | 7 +- .../slate/source/stylesheets/screen.css.scss | 25 +- 24 files changed, 10580 insertions(+), 287 deletions(-) delete mode 100644 samples/rest-notes-slate/slate/Rakefile create mode 100644 samples/rest-notes-slate/slate/Vagrantfile create mode 100755 samples/rest-notes-slate/slate/deploy.sh create mode 100644 samples/rest-notes-slate/slate/lib/multilang.rb rename samples/rest-notes-slate/slate/source/{api-guide.md.erb => api-guide.html.md.erb} (100%) create mode 100644 samples/rest-notes-slate/slate/source/includes/_errors.md create mode 100644 samples/rest-notes-slate/slate/source/index.html.md create mode 100644 samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.js rename samples/rest-notes-slate/slate/source/stylesheets/{_normalize.css => _normalize.scss} (100%) delete mode 100644 samples/rest-notes-slate/slate/source/stylesheets/_syntax.scss.erb diff --git a/samples/rest-notes-slate/slate/.gitignore b/samples/rest-notes-slate/slate/.gitignore index f6fc8c00b..105015835 100644 --- a/samples/rest-notes-slate/slate/.gitignore +++ b/samples/rest-notes-slate/slate/.gitignore @@ -14,9 +14,11 @@ tmp *.DS_STORE build/ .cache +.vagrant +.sass-cache # YARD artifacts .yardoc _yardoc doc/ -.idea/ \ No newline at end of file +.idea/ diff --git a/samples/rest-notes-slate/slate/CHANGELOG.md b/samples/rest-notes-slate/slate/CHANGELOG.md index ee496db6c..be29842dc 100644 --- a/samples/rest-notes-slate/slate/CHANGELOG.md +++ b/samples/rest-notes-slate/slate/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## Version 1.5.0 + +*February 23, 2017* + +- Add [multiple tabs per programming language](https://github.com/lord/slate/wiki/Multiple-language-tabs-per-programming-language) feature +- Upgrade Middleman to add Ruby 1.4.0 compatibility +- Switch default code highlighting color scheme to better highlight JSON +- Various small typo and bug fixes + +## Version 1.4.0 + +*November 24, 2016* + +- Upgrade Middleman and Rouge gems, should hopefully solve a number of bugs +- Update some links in README +- Fix broken Vagrant startup script +- Fix some problems with deploy.sh help message +- Fix bug with language tabs not hiding properly if no error +- Add `!default` to SASS variables +- Fix bug with logo margin +- Bump tested Ruby versions in .travis.yml + +## Version 1.3.3 + +*June 11, 2016* + +Documentation and example changes. + +## Version 1.3.2 + +*February 3, 2016* + +A small bugfix for slightly incorrect background colors on code samples in some cases. + +## Version 1.3.1 + +*January 31, 2016* + +A small bugfix for incorrect whitespace in code blocks. + +## Version 1.3 + +*January 27, 2016* + +We've upgraded Middleman and a number of other dependencies, which should fix quite a few bugs. + +Instead of `rake build` and `rake deploy`, you should now run `bundle exec middleman build --clean` to build your server, and `./deploy.sh` to deploy it to Github Pages. + ## Version 1.2 *June 20, 2015* @@ -21,7 +69,7 @@ ## Version 1.1 -*July 27th, 2014* +*July 27, 2014* **Fixes:** diff --git a/samples/rest-notes-slate/slate/Gemfile b/samples/rest-notes-slate/slate/Gemfile index 0933b9d68..1bff87424 100644 --- a/samples/rest-notes-slate/slate/Gemfile +++ b/samples/rest-notes-slate/slate/Gemfile @@ -1,12 +1,9 @@ source 'https://rubygems.org' # Middleman -gem 'middleman', '~>3.3.10' -gem 'middleman-gh-pages', '~> 0.0.3' -gem 'middleman-syntax', '~> 2.0.0' -gem 'middleman-autoprefixer', '~> 2.4.4' -gem 'rouge', '~> 1.9.0' -gem 'redcarpet', '~> 3.3.2' - -gem 'rake', '~> 10.4.2' -gem 'therubyracer', '~> 0.12.1', platforms: :ruby +gem 'middleman', '~>4.2.1' +gem 'middleman-syntax', '~> 3.0.0' +gem 'middleman-autoprefixer', '~> 2.7.0' +gem "middleman-sprockets", "~> 4.1.0" +gem 'rouge', '~> 2.0.5' +gem 'redcarpet', '~> 3.4.0' diff --git a/samples/rest-notes-slate/slate/Gemfile.lock b/samples/rest-notes-slate/slate/Gemfile.lock index fff5ee10c..adacad9a8 100644 --- a/samples/rest-notes-slate/slate/Gemfile.lock +++ b/samples/rest-notes-slate/slate/Gemfile.lock @@ -1,140 +1,122 @@ GEM remote: https://rubygems.org/ specs: - activesupport (4.1.11) - i18n (~> 0.6, >= 0.6.9) - json (~> 1.7, >= 1.7.7) + activesupport (5.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) minitest (~> 5.1) - thread_safe (~> 0.1) tzinfo (~> 1.1) - autoprefixer-rails (5.2.0.1) + addressable (2.5.0) + public_suffix (~> 2.0, >= 2.0.2) + autoprefixer-rails (6.6.1) execjs - json - celluloid (0.16.0) - timers (~> 4.0.0) - chunky_png (1.3.4) + backports (3.6.8) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.9.1.1) - compass (1.0.3) - chunky_png (~> 1.2) - compass-core (~> 1.0.2) - compass-import-once (~> 1.0.5) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - sass (>= 3.3.13, < 3.5) - compass-core (1.0.3) - multi_json (~> 1.0) - sass (>= 3.3.0, < 3.5) + coffee-script-source (1.12.2) compass-import-once (1.0.5) sass (>= 3.2, < 3.5) + concurrent-ruby (1.0.4) + contracts (0.13.0) + dotenv (2.2.0) erubis (2.7.0) - execjs (2.5.2) - ffi (1.9.8) - haml (4.0.6) + execjs (2.7.0) + fast_blank (1.0.0) + fastimage (2.0.1) + addressable (~> 2) + ffi (1.9.17) + haml (4.0.7) tilt - hike (1.2.3) - hitimes (1.2.2) - hooks (0.4.0) - uber (~> 0.0.4) + hamster (3.0.0) + concurrent-ruby (~> 1.0) + hashie (3.5.1) i18n (0.7.0) - json (1.8.3) - kramdown (1.7.0) - libv8 (3.16.14.7) - listen (2.10.1) - celluloid (~> 0.16.0) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - middleman (3.3.12) + kramdown (1.13.2) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + memoist (0.15.0) + middleman (4.2.1) coffee-script (~> 2.2) - compass (>= 1.0.0, < 2.0.0) compass-import-once (= 1.0.5) - execjs (~> 2.0) haml (>= 4.0.5) kramdown (~> 1.2) - middleman-core (= 3.3.12) - middleman-sprockets (>= 3.1.2) + middleman-cli (= 4.2.1) + middleman-core (= 4.2.1) sass (>= 3.4.0, < 4.0) - uglifier (~> 2.5) - middleman-autoprefixer (2.4.4) - autoprefixer-rails (~> 5.2.0) + middleman-autoprefixer (2.7.1) + autoprefixer-rails (>= 6.5.2, < 7.0.0) middleman-core (>= 3.3.3) - middleman-core (3.3.12) - activesupport (~> 4.1.0) + middleman-cli (4.2.1) + thor (>= 0.17.0, < 2.0) + middleman-core (4.2.1) + activesupport (>= 4.2, < 5.1) + addressable (~> 2.3) + backports (~> 3.6) bundler (~> 1.1) + contracts (~> 0.13.0) + dotenv erubis - hooks (~> 0.3) + execjs (~> 2.0) + fast_blank + fastimage (~> 2.0) + hamster (~> 3.0) + hashie (~> 3.4) i18n (~> 0.7.0) - listen (>= 2.7.9, < 3.0) - padrino-helpers (~> 0.12.3) - rack (>= 1.4.5, < 2.0) - rack-test (~> 0.6.2) - thor (>= 0.15.2, < 2.0) - tilt (~> 1.4.1, < 2.0) - middleman-gh-pages (0.0.3) - rake (> 0.9.3) - middleman-sprockets (3.4.2) - middleman-core (>= 3.3) - sprockets (~> 2.12.1) - sprockets-helpers (~> 1.1.0) - sprockets-sass (~> 1.3.0) - middleman-syntax (2.0.0) - middleman-core (~> 3.2) - rouge (~> 1.0) - minitest (5.7.0) - multi_json (1.11.1) - padrino-helpers (0.12.5) + listen (~> 3.0.0) + memoist (~> 0.14) + padrino-helpers (~> 0.13.0) + parallel + rack (>= 1.4.5, < 3) + sass (>= 3.4) + servolux + tilt (~> 2.0) + uglifier (~> 3.0) + middleman-sprockets (4.1.0) + middleman-core (~> 4.0) + sprockets (>= 3.0) + middleman-syntax (3.0.0) + middleman-core (>= 3.2) + rouge (~> 2.0) + minitest (5.10.1) + padrino-helpers (0.13.3.3) i18n (~> 0.6, >= 0.6.7) - padrino-support (= 0.12.5) - tilt (~> 1.4.1) - padrino-support (0.12.5) + padrino-support (= 0.13.3.3) + tilt (>= 1.4.1, < 3) + padrino-support (0.13.3.3) activesupport (>= 3.1) - rack (1.6.4) - rack-test (0.6.3) - rack (>= 1.0) - rake (10.4.2) - rb-fsevent (0.9.5) - rb-inotify (0.9.5) + parallel (1.10.0) + public_suffix (2.0.5) + rack (2.0.1) + rb-fsevent (0.9.8) + rb-inotify (0.9.8) ffi (>= 0.5.0) - redcarpet (3.3.2) - ref (1.0.5) - rouge (1.9.0) - sass (3.4.14) - sprockets (2.12.3) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-helpers (1.1.0) - sprockets (~> 2.0) - sprockets-sass (1.3.1) - sprockets (~> 2.0) - tilt (~> 1.1) - therubyracer (0.12.2) - libv8 (~> 3.16.14.0) - ref - thor (0.19.1) + redcarpet (3.4.0) + rouge (2.0.7) + sass (3.4.23) + servolux (0.12.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + thor (0.19.4) thread_safe (0.3.5) - tilt (1.4.1) - timers (4.0.1) - hitimes + tilt (2.0.6) tzinfo (1.2.2) thread_safe (~> 0.1) - uber (0.0.13) - uglifier (2.7.1) - execjs (>= 0.3.0) - json (>= 1.8.0) + uglifier (3.0.4) + execjs (>= 0.3.0, < 3) PLATFORMS ruby DEPENDENCIES - middleman (~> 3.3.10) - middleman-autoprefixer (~> 2.4.4) - middleman-gh-pages (~> 0.0.3) - middleman-syntax (~> 2.0.0) - rake (~> 10.4.2) - redcarpet (~> 3.3.2) - rouge (~> 1.9.0) - therubyracer (~> 0.12.1) + middleman (~> 4.2.1) + middleman-autoprefixer (~> 2.7.0) + middleman-sprockets (~> 4.1.0) + middleman-syntax (~> 3.0.0) + redcarpet (~> 3.4.0) + rouge (~> 2.0.5) + +BUNDLED WITH + 1.14.3 diff --git a/samples/rest-notes-slate/slate/README.md b/samples/rest-notes-slate/slate/README.md index 2f042e7f2..38acc9a6d 100644 --- a/samples/rest-notes-slate/slate/README.md +++ b/samples/rest-notes-slate/slate/README.md @@ -1,34 +1,33 @@ -Slate -======== +

          + Slate: API Documentation Generator +
          + Build Status +

          -[![Build Status](https://travis-ci.org/tripit/slate.svg?branch=master)](https://travis-ci.org/tripit/slate) [![Dependency Status](https://gemnasium.com/tripit/slate.png)](https://gemnasium.com/tripit/slate) +

          Slate helps you create beautiful, intelligent, responsive API documentation.

          -Slate helps you create beautiful API documentation. Think of it as an intelligent, responsive documentation template for your API. +

          Screenshot of Example Documentation created with Slate

          -Screenshot of Example Documentation created with Slate - -*The example above was created with Slate. Check it out at [tripit.github.io/slate](http://tripit.github.io/slate).* +

          The example above was created with Slate. Check it out at lord.github.io/slate.

          Features ------------ -* **Clean, intuitive design** — with Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [Paypal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even print. +* **Clean, intuitive design** — With Slate, the description of your API is on the left side of your documentation, and all the code examples are on the right side. Inspired by [Stripe's](https://stripe.com/docs/api) and [Paypal's](https://developer.paypal.com/webapps/developer/docs/api/) API docs. Slate is responsive, so it looks great on tablets, phones, and even in print. -* **Everything on a single page** — gone are the days where your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy. +* **Everything on a single page** — Gone are the days when your users had to search through a million pages to find what they wanted. Slate puts the entire documentation on a single page. We haven't sacrificed linkability, though. As you scroll, your browser's hash will update to the nearest header, so linking to a particular point in the documentation is still natural and easy. -* **Slate is just Markdown** — when you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown — even the code samples are just Markdown code blocks! +* **Slate is just Markdown** — When you write docs with Slate, you're just writing Markdown, which makes it simple to edit and understand. Everything is written in Markdown — even the code samples are just Markdown code blocks. -* **Write code samples in multiple languages** — if your API has bindings in multiple programming languages, you easily put in tabs to switch between them. In your document, you'll distinguish different languages by specifying the language name at the top of each code block, just like with Github Flavored Markdown! +* **Write code samples in multiple languages** — If your API has bindings in multiple programming languages, you can easily put in tabs to switch between them. In your document, you'll distinguish different languages by specifying the language name at the top of each code block, just like with Github Flavored Markdown. -* **Out-of-the-box syntax highlighting** for [almost 60 languages](http://rouge.jayferd.us/demo), no configuration required. +* **Out-of-the-box syntax highlighting** for [over 100 languages](https://github.com/jneen/rouge/wiki/List-of-supported-languages-and-lexers), no configuration required. * **Automatic, smoothly scrolling table of contents** on the far left of the page. As you scroll, it displays your current position in the document. It's fast, too. We're using Slate at TripIt to build documentation for our new API, where our table of contents has over 180 entries. We've made sure that the performance remains excellent, even for larger documents. -* **Let your users update your documentation for you** — by default, your Slate-generated documentation is hosted in a public Github repository. Not only does this mean you get free hosting for your docs with Github Pages, but it also makes it's simple for other developers to make pull requests to your docs if they find typos or other problems. Of course, if you don't want to, you're welcome to not use Github and host your docs elsewhere! - -Getting starting with Slate is super easy! Simply fork this repository, and then follow the instructions below. Or, if you'd like to check out what Slate is capable of, take a look at the [sample docs](http://tripit.github.io/slate). +* **Let your users update your documentation for you** — By default, your Slate-generated documentation is hosted in a public Github repository. Not only does this mean you get free hosting for your docs with Github Pages, but it also makes it simple for other developers to make pull requests to your docs if they find typos or other problems. Of course, if you don't want to use GitHub, you're also welcome to host your docs elsewhere. - +Getting started with Slate is super easy! Simply fork this repository and follow the instructions below. Or, if you'd like to check out what Slate is capable of, take a look at the [sample docs](http://lord.github.io/slate). Getting Started with Slate ------------------------------ @@ -38,84 +37,68 @@ Getting Started with Slate You're going to need: - **Linux or OS X** — Windows may work, but is unsupported. - - **Ruby, version 1.9.3 or newer** + - **Ruby, version 2.2.5 or newer** - **Bundler** — If Ruby is already installed, but the `bundle` command doesn't work, just run `gem install bundler` in a terminal. ### Getting Set Up - 1. Fork this repository on Github. - 2. Clone *your forked repository* (not our original one) to your hard drive with `git clone https://github.com/YOURUSERNAME/slate.git` - 3. `cd slate` - 4. Install all dependencies: `bundle install` - 5. Start the test server: `bundle exec middleman server` - -Or use the included Dockerfile! (must install Docker first) +1. Fork this repository on Github. +2. Clone *your forked repository* (not our original one) to your hard drive with `git clone https://github.com/YOURUSERNAME/slate.git` +3. `cd slate` +4. Initialize and start Slate. You can either do this locally, or with Vagrant: ```shell -docker build -t slate . -docker run -d -p 4567:4567 --name slate -v $(pwd)/source:/app/source slate +# either run this to run locally +bundle install +bundle exec middleman server + +# OR run this to run with vagrant +vagrant up ``` -You can now see the docs at . Whoa! That was fast! +You can now see the docs at http://localhost:4567. Whoa! That was fast! -*Note: if you're using the Docker setup on OSX, the docs will be -availalable at the output of `boot2docker ip` instead of `localhost:4567`.* +Now that Slate is all set up on your machine, you'll probably want to learn more about [editing Slate markdown](https://github.com/lord/slate/wiki/Markdown-Syntax), or [how to publish your docs](https://github.com/lord/slate/wiki/Deploying-Slate). -Now that Slate is all set up your machine, you'll probably want to learn more about [editing Slate markdown](https://github.com/tripit/slate/wiki/Markdown-Syntax), or [how to publish your docs](https://github.com/tripit/slate/wiki/Deploying-Slate). +If you'd prefer to use Docker, instructions are available [in the wiki](https://github.com/lord/slate/wiki/Docker). -Examples of Slate in the Wild +Companies Using Slate --------------------------------- -* [Travis-CI's API docs](http://docs.travis-ci.com/api/) -* [Mozilla's localForage docs](http://mozilla.github.io/localForage/) -* [Mozilla Recroom](http://mozilla.github.io/recroom/) -* [ChaiOne Gameplan API docs](http://chaione.github.io/gameplanb2b/#introduction) -* [Drcaban's Build a Quine tutorial](http://drcabana.github.io/build-a-quine/#introduction) -* [PricePlow API docs](https://www.priceplow.com/api/documentation) -* [Emerging Threats API docs](http://apidocs.emergingthreats.net/) -* [Appium docs](http://appium.io/slate/en/master) -* [Golazon Developer](http://developer.golazon.com) -* [Dwolla API docs](https://docs.dwolla.com/) -* [RozpisyZapasu API docs](http://www.rozpisyzapasu.cz/dev/api/) -* [Codestar Framework Docs](http://codestarframework.com/documentation/) -* [Buddycloud API](http://buddycloud.com/api) -* [Crafty Clicks API](https://craftyclicks.co.uk/api/) -* [Paracel API Reference](http://paracel.io/docs/api_reference.html) -* [Switch Payments Documentation](http://switchpayments.com/docs/) & [API](http://switchpayments.com/developers/) -* [Coinbase API Reference](https://developers.coinbase.com/api) -* [Whispir.io API](https://whispir.github.io/api) -* [NASA API](https://data.nasa.gov/developer/external/planetary/) -* [CardPay API](https://developers.cardpay.com/) -* [IBM Cloudant](https://docs-testb.cloudant.com/content-review/_design/couchapp/index.html) -* [Bitrix basis components](http://bbc.bitrix.expert/) -* [viagogo API Documentation](http://developer.viagogo.net/) -* [Fidor Bank API Documentation](http://docs.fidor.de/) -* [Market Prophit API Documentation](http://developer.marketprophit.com/) -* [OAuth.io API Documentation](http://docs.oauth.io/) -* [Aircall for Developers](http://developer.aircall.io/) -* [SupportKit API Docs](http://docs.supportkit.io/) -* [SocialRadar's LocationKit Docs](https://docs.locationkit.io/) -* [SafetyCulture API Documentation](https://developer.safetyculture.io/) -* [hosting.de API Documentation](https://www.hosting.de/docs/api/) - -(Feel free to add your site to this list in a pull request!) +* [NASA](https://api.nasa.gov) +* [IBM](https://docs.cloudant.com/api.html) +* [Sony](http://developers.cimediacloud.com) +* [Mozilla](http://localforage.github.io/localForage/) +* [Best Buy](https://bestbuyapis.github.io/api-documentation/) +* [Travis-CI](https://docs.travis-ci.com/api/) +* [Greenhouse](https://developers.greenhouse.io/harvest.html) +* [Woocommerce](http://woocommerce.github.io/woocommerce-rest-api-docs/) +* [Appium](http://appium.io/slate/en/master) +* [Dwolla](https://docs.dwolla.com/) +* [Clearbit](https://clearbit.com/docs) +* [Coinbase](https://developers.coinbase.com/api) +* [Parrot Drones](http://developer.parrot.com/docs/bebop/) +* [Fidor Bank](http://docs.fidor.de/) +* [Scale](https://docs.scaleapi.com/) + +You can view more in [the list on the wiki](https://github.com/lord/slate/wiki/Slate-in-the-Wild). Need Help? Found a bug? -------------------- -Just [submit a issue](https://github.com/tripit/slate/issues) to the Slate Github if you need any help. And, of course, feel free to submit pull requests with bug fixes or changes. - +[Submit an issue](https://github.com/lord/slate/issues) to the Slate Github if you need any help. And, of course, feel free to submit pull requests with bug fixes or changes. Contributors -------------------- -Slate was built by [Robert Lord](https://lord.io) while at [TripIt](http://tripit.com). +Slate was built by [Robert Lord](https://lord.io) while interning at [TripIt](https://www.tripit.com/). Thanks to the following people who have submitted major pull requests: - [@chrissrogers](https://github.com/chrissrogers) - [@bootstraponline](https://github.com/bootstraponline) - [@realityking](https://github.com/realityking) +- [@cvkef](https://github.com/cvkef) Also, thanks to [Sauce Labs](http://saucelabs.com) for helping sponsor the project. @@ -124,5 +107,5 @@ Special Thanks - [Middleman](https://github.com/middleman/middleman) - [jquery.tocify.js](https://github.com/gfranko/jquery.tocify.js) - [middleman-syntax](https://github.com/middleman/middleman-syntax) -- [middleman-gh-pages](https://github.com/neo/middleman-gh-pages) +- [middleman-gh-pages](https://github.com/edgecase/middleman-gh-pages) - [Font Awesome](http://fortawesome.github.io/Font-Awesome/) diff --git a/samples/rest-notes-slate/slate/Rakefile b/samples/rest-notes-slate/slate/Rakefile deleted file mode 100644 index 6a952e1e9..000000000 --- a/samples/rest-notes-slate/slate/Rakefile +++ /dev/null @@ -1,6 +0,0 @@ -require 'middleman-gh-pages' -require 'rake/clean' - -CLOBBER.include('build') - -task :default => [:build] diff --git a/samples/rest-notes-slate/slate/Vagrantfile b/samples/rest-notes-slate/slate/Vagrantfile new file mode 100644 index 000000000..43b1f9954 --- /dev/null +++ b/samples/rest-notes-slate/slate/Vagrantfile @@ -0,0 +1,39 @@ +Vagrant.configure(2) do |config| + config.vm.box = "ubuntu/trusty64" + config.vm.network :forwarded_port, guest: 4567, host: 4567 + + config.vm.provision "bootstrap", + type: "shell", + inline: <<-SHELL + sudo apt-get update + sudo apt-get install -yq ruby2.0 ruby2.0-dev pkg-config build-essential nodejs git libxml2-dev libxslt-dev + sudo apt-get autoremove -yq + gem2.0 install --no-ri --no-rdoc bundler + SHELL + + # add the local user git config to the vm + config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig" + + config.vm.provision "install", + type: "shell", + privileged: false, + inline: <<-SHELL + echo "==============================================" + echo "Installing app dependencies" + cd /vagrant + bundle config build.nokogiri --use-system-libraries + bundle install + SHELL + + config.vm.provision "run", + type: "shell", + privileged: false, + run: "always", + inline: <<-SHELL + echo "==============================================" + echo "Starting up middleman at http://localhost:4567" + echo "If it does not come up, check the ~/middleman.log file for any error messages" + cd /vagrant + bundle exec middleman server --force-polling --latency=1 &> ~/middleman.log & + SHELL +end diff --git a/samples/rest-notes-slate/slate/config.rb b/samples/rest-notes-slate/slate/config.rb index fdcb21f32..0cb5fea49 100644 --- a/samples/rest-notes-slate/slate/config.rb +++ b/samples/rest-notes-slate/slate/config.rb @@ -17,6 +17,11 @@ # Activate the syntax highlighter activate :syntax +ready do + require './lib/multilang.rb' +end + +activate :sprockets activate :autoprefixer do |config| config.browsers = ['last 2 version', 'Firefox ESR'] @@ -33,9 +38,15 @@ set :build_dir, '../build/docs' configure :build do + # If you're having trouble with Middleman hanging, commenting + # out the following two lines has been known to help activate :minify_css activate :minify_javascript # activate :relative_assets # activate :asset_hash # activate :gzip end + +# Deploy Configuration +# If you want Middleman to listen on a different port, you can set that below +set :port, 4567 diff --git a/samples/rest-notes-slate/slate/deploy.sh b/samples/rest-notes-slate/slate/deploy.sh new file mode 100755 index 000000000..909a9d908 --- /dev/null +++ b/samples/rest-notes-slate/slate/deploy.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +set -o errexit #abort if any command fails +me=$(basename "$0") + +help_message="\ +Usage: $me [-c FILE] [] +Deploy generated files to a git branch. + +Options: + + -h, --help Show this help information. + -v, --verbose Increase verbosity. Useful for debugging. + -e, --allow-empty Allow deployment of an empty directory. + -m, --message MESSAGE Specify the message used when committing on the + deploy branch. + -n, --no-hash Don't append the source commit's hash to the deploy + commit's message. +" + +bundle exec middleman build --clean + +parse_args() { + # Set args from a local environment file. + if [ -e ".env" ]; then + source .env + fi + + # Parse arg flags + # If something is exposed as an environment variable, set/overwrite it + # here. Otherwise, set/overwrite the internal variable instead. + while : ; do + if [[ $1 = "-h" || $1 = "--help" ]]; then + echo "$help_message" + return 0 + elif [[ $1 = "-v" || $1 = "--verbose" ]]; then + verbose=true + shift + elif [[ $1 = "-e" || $1 = "--allow-empty" ]]; then + allow_empty=true + shift + elif [[ ( $1 = "-m" || $1 = "--message" ) && -n $2 ]]; then + commit_message=$2 + shift 2 + elif [[ $1 = "-n" || $1 = "--no-hash" ]]; then + GIT_DEPLOY_APPEND_HASH=false + shift + else + break + fi + done + + # Set internal option vars from the environment and arg flags. All internal + # vars should be declared here, with sane defaults if applicable. + + # Source directory & target branch. + deploy_directory=build + deploy_branch=gh-pages + + #if no user identity is already set in the current git environment, use this: + default_username=${GIT_DEPLOY_USERNAME:-deploy.sh} + default_email=${GIT_DEPLOY_EMAIL:-} + + #repository to deploy to. must be readable and writable. + repo=origin + + #append commit hash to the end of message by default + append_hash=${GIT_DEPLOY_APPEND_HASH:-true} +} + +main() { + parse_args "$@" + + enable_expanded_output + + if ! git diff --exit-code --quiet --cached; then + echo Aborting due to uncommitted changes in the index >&2 + return 1 + fi + + commit_title=`git log -n 1 --format="%s" HEAD` + commit_hash=` git log -n 1 --format="%H" HEAD` + + #default commit message uses last title if a custom one is not supplied + if [[ -z $commit_message ]]; then + commit_message="publish: $commit_title" + fi + + #append hash to commit message unless no hash flag was found + if [ $append_hash = true ]; then + commit_message="$commit_message"$'\n\n'"generated from commit $commit_hash" + fi + + previous_branch=`git rev-parse --abbrev-ref HEAD` + + if [ ! -d "$deploy_directory" ]; then + echo "Deploy directory '$deploy_directory' does not exist. Aborting." >&2 + return 1 + fi + + # must use short form of flag in ls for compatibility with OS X and BSD + if [[ -z `ls -A "$deploy_directory" 2> /dev/null` && -z $allow_empty ]]; then + echo "Deploy directory '$deploy_directory' is empty. Aborting. If you're sure you want to deploy an empty tree, use the --allow-empty / -e flag." >&2 + return 1 + fi + + if git ls-remote --exit-code $repo "refs/heads/$deploy_branch" ; then + # deploy_branch exists in $repo; make sure we have the latest version + + disable_expanded_output + git fetch --force $repo $deploy_branch:$deploy_branch + enable_expanded_output + fi + + # check if deploy_branch exists locally + if git show-ref --verify --quiet "refs/heads/$deploy_branch" + then incremental_deploy + else initial_deploy + fi + + restore_head +} + +initial_deploy() { + git --work-tree "$deploy_directory" checkout --orphan $deploy_branch + git --work-tree "$deploy_directory" add --all + commit+push +} + +incremental_deploy() { + #make deploy_branch the current branch + git symbolic-ref HEAD refs/heads/$deploy_branch + #put the previously committed contents of deploy_branch into the index + git --work-tree "$deploy_directory" reset --mixed --quiet + git --work-tree "$deploy_directory" add --all + + set +o errexit + diff=$(git --work-tree "$deploy_directory" diff --exit-code --quiet HEAD --)$? + set -o errexit + case $diff in + 0) echo No changes to files in $deploy_directory. Skipping commit.;; + 1) commit+push;; + *) + echo git diff exited with code $diff. Aborting. Staying on branch $deploy_branch so you can debug. To switch back to master, use: git symbolic-ref HEAD refs/heads/master && git reset --mixed >&2 + return $diff + ;; + esac +} + +commit+push() { + set_user_id + git --work-tree "$deploy_directory" commit -m "$commit_message" + + disable_expanded_output + #--quiet is important here to avoid outputting the repo URL, which may contain a secret token + git push --quiet $repo $deploy_branch + enable_expanded_output +} + +#echo expanded commands as they are executed (for debugging) +enable_expanded_output() { + if [ $verbose ]; then + set -o xtrace + set +o verbose + fi +} + +#this is used to avoid outputting the repo URL, which may contain a secret token +disable_expanded_output() { + if [ $verbose ]; then + set +o xtrace + set -o verbose + fi +} + +set_user_id() { + if [[ -z `git config user.name` ]]; then + git config user.name "$default_username" + fi + if [[ -z `git config user.email` ]]; then + git config user.email "$default_email" + fi +} + +restore_head() { + if [[ $previous_branch = "HEAD" ]]; then + #we weren't on any branch before, so just set HEAD back to the commit it was on + git update-ref --no-deref HEAD $commit_hash $deploy_branch + else + git symbolic-ref HEAD refs/heads/$previous_branch + fi + + git reset --mixed +} + +filter() { + sed -e "s|$repo|\$repo|g" +} + +sanitize() { + "$@" 2> >(filter 1>&2) | filter +} + +[[ $1 = --source-only ]] || main "$@" diff --git a/samples/rest-notes-slate/slate/lib/multilang.rb b/samples/rest-notes-slate/slate/lib/multilang.rb new file mode 100644 index 000000000..624c6e495 --- /dev/null +++ b/samples/rest-notes-slate/slate/lib/multilang.rb @@ -0,0 +1,12 @@ +module Multilang + def block_code(code, full_lang_name) + parts = full_lang_name.split('--') + rouge_lang_name = parts[0] || "" + super(code, rouge_lang_name).sub("highlight #{rouge_lang_name}") do |match| + match + " tab-" + full_lang_name + end + end +end + +require 'middleman-core/renderers/redcarpet' +Middleman::Renderers::MiddlemanRedcarpetHTML.send :include, Multilang diff --git a/samples/rest-notes-slate/slate/source/api-guide.md.erb b/samples/rest-notes-slate/slate/source/api-guide.html.md.erb similarity index 100% rename from samples/rest-notes-slate/slate/source/api-guide.md.erb rename to samples/rest-notes-slate/slate/source/api-guide.html.md.erb diff --git a/samples/rest-notes-slate/slate/source/images/logo.png b/samples/rest-notes-slate/slate/source/images/logo.png index fa1f13da8193dacca747df56cc1f7bb0bc111a7e..68a478fae3611d878af18c575cf8218b818f41db 100644 GIT binary patch literal 22317 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
          hg5Z+!{Sa)O9QNxi$tfwtDZos~b$g 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 F`9JqINYVfR literal 3507 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 diff --git a/samples/rest-notes-slate/slate/source/includes/_errors.md b/samples/rest-notes-slate/slate/source/includes/_errors.md new file mode 100644 index 000000000..267e352f3 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/includes/_errors.md @@ -0,0 +1,20 @@ +# Errors + + + +The Kittn API uses the following error codes: + + +Error Code | Meaning +---------- | ------- +400 | Bad Request -- Your request sucks +401 | Unauthorized -- Your API key is wrong +403 | Forbidden -- The kitten requested is hidden for administrators only +404 | Not Found -- The specified kitten could not be found +405 | Method Not Allowed -- You tried to access a kitten with an invalid method +406 | Not Acceptable -- You requested a format that isn't json +410 | Gone -- The kitten requested has been removed from our servers +418 | I'm a teapot +429 | Too Many Requests -- You're requesting too many kittens! Slow down! +500 | Internal Server Error -- We had a problem with our server. Try again later. +503 | Service Unavailable -- We're temporarily offline for maintenance. Please try again later. diff --git a/samples/rest-notes-slate/slate/source/index.html.md b/samples/rest-notes-slate/slate/source/index.html.md new file mode 100644 index 000000000..fc26b1964 --- /dev/null +++ b/samples/rest-notes-slate/slate/source/index.html.md @@ -0,0 +1,189 @@ +--- +title: API Reference + +language_tabs: + - shell + - ruby + - python + - javascript + +toc_footers: + - Sign Up for a Developer Key + - Documentation Powered by Slate + +includes: + - errors + +search: true +--- + +# Introduction + +Welcome to the Kittn API! You can use our API to access Kittn API endpoints, which can get information on various cats, kittens, and breeds in our database. + +We have language bindings in Shell, Ruby, and Python! You can view code examples in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right. + +This example API documentation page was created with [Slate](https://github.com/tripit/slate). Feel free to edit it and use it as a base for your own API's documentation. + +# Authentication + +> To authorize, use this code: + +```ruby +require 'kittn' + +api = Kittn::APIClient.authorize!('meowmeowmeow') +``` + +```python +import kittn + +api = kittn.authorize('meowmeowmeow') +``` + +```shell +# With shell, you can just pass the correct header with each request +curl "api_endpoint_here" + -H "Authorization: meowmeowmeow" +``` + +```javascript +const kittn = require('kittn'); + +let api = kittn.authorize('meowmeowmeow'); +``` + +> Make sure to replace `meowmeowmeow` with your API key. + +Kittn uses API keys to allow access to the API. You can register a new Kittn API key at our [developer portal](http://example.com/developers). + +Kittn expects for the API key to be included in all API requests to the server in a header that looks like the following: + +`Authorization: meowmeowmeow` + + + +# Kittens + +## Get All Kittens + +```ruby +require 'kittn' + +api = Kittn::APIClient.authorize!('meowmeowmeow') +api.kittens.get +``` + +```python +import kittn + +api = kittn.authorize('meowmeowmeow') +api.kittens.get() +``` + +```shell +curl "http://example.com/api/kittens" + -H "Authorization: meowmeowmeow" +``` + +```javascript +const kittn = require('kittn'); + +let api = kittn.authorize('meowmeowmeow'); +let kittens = api.kittens.get(); +``` + +> The above command returns JSON structured like this: + +```json +[ + { + "id": 1, + "name": "Fluffums", + "breed": "calico", + "fluffiness": 6, + "cuteness": 7 + }, + { + "id": 2, + "name": "Max", + "breed": "unknown", + "fluffiness": 5, + "cuteness": 10 + } +] +``` + +This endpoint retrieves all kittens. + +### HTTP Request + +`GET http://example.com/api/kittens` + +### Query Parameters + +Parameter | Default | Description +--------- | ------- | ----------- +include_cats | false | If set to true, the result will also include cats. +available | true | If set to false, the result will include kittens that have already been adopted. + + + +## Get a Specific Kitten + +```ruby +require 'kittn' + +api = Kittn::APIClient.authorize!('meowmeowmeow') +api.kittens.get(2) +``` + +```python +import kittn + +api = kittn.authorize('meowmeowmeow') +api.kittens.get(2) +``` + +```shell +curl "http://example.com/api/kittens/2" + -H "Authorization: meowmeowmeow" +``` + +```javascript +const kittn = require('kittn'); + +let api = kittn.authorize('meowmeowmeow'); +let max = api.kittens.get(2); +``` + +> The above command returns JSON structured like this: + +```json +{ + "id": 2, + "name": "Max", + "breed": "unknown", + "fluffiness": 5, + "cuteness": 10 +} +``` + +This endpoint retrieves a specific kitten. + + + +### HTTP Request + +`GET http://example.com/kittens/` + +### URL Parameters + +Parameter | Description +--------- | ----------- +ID | The ID of the kitten to retrieve + diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js b/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js index 1a124bb68..992180b76 100644 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_lang.js @@ -1,3 +1,5 @@ +//= require ../lib/_jquery + /* Copyright 2008-2013 Concur Technologies, Inc. @@ -28,9 +30,11 @@ under the License. $(".lang-selector a").removeClass('active'); $(".lang-selector a[data-language-name='" + language + "']").addClass('active'); for (var i=0; i < languages.length; i++) { - $(".highlight." + languages[i]).hide(); + $(".highlight.tab-" + languages[i]).hide(); + $(".lang-specific." + languages[i]).hide(); } - $(".highlight." + language).show(); + $(".highlight.tab-" + language).show(); + $(".lang-specific." + language).show(); global.toc.calculateHeights(); diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js index 91f38a04e..5ace538d8 100644 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_search.js +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_search.js @@ -1,4 +1,5 @@ //= require ../lib/_lunr +//= require ../lib/_jquery //= require ../lib/_jquery.highlight (function () { 'use strict'; diff --git a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js index bc2aa3e1f..21d080006 100644 --- a/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js +++ b/samples/rest-notes-slate/slate/source/javascripts/app/_toc.js @@ -1,3 +1,4 @@ +//= require ../lib/_jquery //= require ../lib/_jquery_ui //= require ../lib/_jquery.tocify //= require ../lib/_imagesloaded.min @@ -47,6 +48,7 @@ $(function() { makeToc(); animate(); + setupLanguages($('body').data('languages')); $('.content').imagesLoaded( function() { global.toc.calculateHeights(); }); diff --git a/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.js b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.js new file mode 100644 index 000000000..b78120e6a --- /dev/null +++ b/samples/rest-notes-slate/slate/source/javascripts/lib/_jquery.js @@ -0,0 +1,9831 @@ +/*! + * jQuery JavaScript Library v2.2.0 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2016-01-08T20:02Z + */ + +(function( global, factory ) { + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Support: Firefox 18+ +// Can't be in strict mode, several libs including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +//"use strict"; +var arr = []; + +var document = window.document; + +var slice = arr.slice; + +var concat = arr.concat; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var support = {}; + + + +var + version = "2.2.0", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android<4.1 + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num != null ? + + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : + + // Return all the elements in a clean array + slice.call( this ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = jQuery.isArray( copy ) ) ) ) { + + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray( src ) ? src : []; + + } else { + clone = src && jQuery.isPlainObject( src ) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isFunction: function( obj ) { + return jQuery.type( obj ) === "function"; + }, + + isArray: Array.isArray, + + isWindow: function( obj ) { + return obj != null && obj === obj.window; + }, + + isNumeric: function( obj ) { + + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + // adding 1 corrects loss of precision from parseFloat (#15100) + var realStringObj = obj && obj.toString(); + return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0; + }, + + isPlainObject: function( obj ) { + + // Not plain objects: + // - Any object or value whose internal [[Class]] property is not "[object Object]" + // - DOM nodes + // - window + if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.constructor && + !hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) { + return false; + } + + // If the function hasn't returned already, we're confident that + // |obj| is a plain object, created by {} or constructed with new Object + return true; + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android<4.0, iOS<6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function( code ) { + var script, + indirect = eval; + + code = jQuery.trim( code ); + + if ( code ) { + + // If the code includes a valid, prologue position + // strict mode pragma, execute code by injecting a + // script tag into the document. + if ( code.indexOf( "use strict" ) === 1 ) { + script = document.createElement( "script" ); + script.text = code; + document.head.appendChild( script ).parentNode.removeChild( script ); + } else { + + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + + indirect( code ); + } + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Support: IE9-11+ + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // Support: Android<4.1 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +// JSHint would error on this code due to the Symbol not being defined in ES5. +// Defining this global in .jshintrc would create a danger of using the global +// unguarded in another place, it seems safer to just disable JSHint for these +// three lines. +/* jshint ignore: start */ +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} +/* jshint ignore: end */ + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: iOS 8.2 (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.2.1 + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-10-17 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // General-purpose constants + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf as it's faster than native + // http://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }; + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, nidselect, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; + + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { + + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']"; + while ( i-- ) { + groups[i] = nidselect + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, parent, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9-11, Edge + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + if ( (parent = document.defaultView) && parent.top !== parent ) { + // Support: IE 11 + if ( parent.addEventListener ) { + parent.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", unloadHandler ); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( document.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + }); + + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var m = context.getElementById( id ); + return m ? [ m ] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + docElem.appendChild( div ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowcapture^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push("~="); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibing-combinator selector` fails + if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push(".#.+[+~]"); + } + }); + + assert(function( div ) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === document ? -1 : + b === document ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + !compilerCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch (e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + // Use previously-cached element index if available + if ( useCache ) { + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + // Don't keep the element (issue #299) + input[0] = null; + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + + if ( (oldCache = uniqueCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context === document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + if ( !context && elem.ownerDocument !== document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context || document, xml) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ ); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + } ); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + // Support: Blackberry 4.6 + // gEBID returns nodes no longer in the document (#6963) + if ( elem && elem.parentNode ) { + + // Inject the element directly into the jQuery object + this.length = 1; + this[ 0 ] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( pos ? + pos.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnotwhite = ( /\S+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( jQuery.isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], + [ "notify", "progress", jQuery.Callbacks( "memory" ) ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this === promise ? newDefer.promise() : this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( function() { + + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || + ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. + // If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // Add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .progress( updateFunc( i, progressContexts, progressValues ) ) + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ); + } else { + --remaining; + } + } + } + + // If we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +} ); + + +// The deferred used on DOM ready +var readyList; + +jQuery.fn.ready = function( fn ) { + + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +} ); + +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called + // after the browser event has already occurred. + // Support: IE9-10 only + // Older IE sometimes signals "interactive" too soon + if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + + } else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); + } + } + return readyList.promise( obj ); +}; + +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[ 0 ], key ) : emptyGet; +}; +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + register: function( owner, initial ) { + var value = initial || {}; + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable, non-writable property + // configurability must be true to allow the property to be + // deleted with the delete operator + } else { + Object.defineProperty( owner, this.expando, { + value: value, + writable: true, + configurable: true + } ); + } + return owner[ this.expando ]; + }, + cache: function( owner ) { + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( !acceptData( owner ) ) { + return {}; + } + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + if ( typeof data === "string" ) { + cache[ data ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ prop ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + owner[ this.expando ] && owner[ this.expando ][ key ]; + }, + access: function( owner, key, value ) { + var stored; + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + stored = this.get( owner, key ); + + return stored !== undefined ? + stored : this.get( owner, jQuery.camelCase( key ) ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, name, camel, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key === undefined ) { + this.register( owner ); + + } else { + + // Support array or space separated string of keys + if ( jQuery.isArray( key ) ) { + + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = key.concat( key.map( jQuery.camelCase ) ); + } else { + camel = jQuery.camelCase( key ); + + // Try the string as a key before any manipulation + if ( key in cache ) { + name = [ key, camel ]; + } else { + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match( rnotwhite ) || [] ); + } + } + + i = name.length; + + while ( i-- ) { + delete cache[ name[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <= 35-45+ + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://code.google.com/p/chromium/issues/detail?id=378607 + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data, camelKey; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // with the key as-is + data = dataUser.get( elem, key ) || + + // Try to find dashed key if it exists (gh-2779) + // This is for 2.2.x only + dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() ); + + if ( data !== undefined ) { + return data; + } + + camelKey = jQuery.camelCase( key ); + + // Attempt to get data from the cache + // with the key camelized + data = dataUser.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + camelKey = jQuery.camelCase( key ); + this.each( function() { + + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = dataUser.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + dataUser.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf( "-" ) > -1 && data !== undefined ) { + dataUser.set( this, key, value ); + } + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || + !jQuery.contains( elem.ownerDocument, elem ); + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, + scale = 1, + maxIterations = 20, + currentValue = tween ? + function() { return tween.cur(); } : + function() { return jQuery.css( elem, prop, "" ); }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + do { + + // If previous iteration zeroed out, double until we get *something*. + // Use string for doubling so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + initialInUnit = initialInUnit / scale; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // Break the loop if scale is unchanged or perfect, or if we've just had enough. + } while ( + scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations + ); + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([\w:-]+)/ ); + +var rscriptType = ( /^$|\/(?:java|ecma)script/i ); + + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // Support: IE9 + option: [ 1, "" ], + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
          " ], + col: [ 2, "", "
          " ], + tr: [ 2, "", "
          " ], + td: [ 3, "", "
          " ], + + _default: [ 0, "", "" ] +}; + +// Support: IE9 +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + + +function getAll( context, tag ) { + + // Support: IE9-11+ + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== "undefined" ? + context.querySelectorAll( tag || "*" ) : + []; + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0-4.3, Safari<=5.1 + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Safari<=5.1, Android<4.2 + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<=11+ + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +} )(); + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE9 +// See #13393 for more info +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = {}; + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or 2) have namespace(s) + // a subset or equal to those in the bound event (both can have no namespace). + if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Support (at least): Chrome, IE9 + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // + // Support: Firefox<=42+ + // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) + if ( delegateCount && cur.nodeType && + ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push( { elem: cur, handlers: matches } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + + "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split( " " ), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " + + "screenX screenY toElement" ).split( " " ), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - + ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - + ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome<28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android<4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://code.google.com/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, + + // Support: IE 10-11, Edge 10240+ + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +function manipulationTarget( elem, content ) { + if ( jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return elem.getElementsByTagName( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.access( src ); + pdataCur = dataPriv.set( dest, pdataOld ); + events = pdataOld.events; + + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( isFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html.replace( rxhtmlTag, "<$1>" ); + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + + // Keep domManip exposed until 3.0 (gh-2225) + domManip: domManip, + + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); + + +var iframe, + elemdisplay = { + + // Support: Firefox + // We have to pre-define these values for FF (#10227) + HTML: "block", + BODY: "block" + }; + +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ + +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + + display = jQuery.css( elem[ 0 ], "display" ); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; +} + +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = ( iframe || jQuery( "