From 4f850cddd920ca1425c2ae05afff88287dce7b4f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 21 Jul 2015 15:38:39 +0100 Subject: [PATCH] Use a template engine to produce the documentation snippets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a new TemplateEngine abstraction that is used to produce the documentation snippets. A default JMustache-based implementation is provided. JMustache has been repackaged and embedded to prevent unwanted conflicts and side-effects. By default, snippet templates are loaded from the classpath in the org.springframework.restdocs.templates package. Default snippet templates are provided for all of the snippets that can be generated. Each of these templates is named after the snippet that it will produce – the snippet {name}.adoc is produced by the snippet template default-{name}.snippet. A snippet named {name}.snippet, if present, will be used in preference to the default snippet, thereby allowing the default snippets to be overriden. --- build.gradle | 2 + .../docs/asciidoc/customizing-snippets.adoc | 12 + docs/src/docs/asciidoc/index.adoc | 5 +- .../asciidoc/working-with-asciidoctor.adoc | 4 +- spring-restdocs/build.gradle | 35 ++- .../config/RestDocumentationConfigurer.java | 29 ++- .../restdocs/curl/CurlDocumentation.java | 134 +++++----- .../restdocs/http/HttpDocumentation.java | 243 ++++++++++-------- .../hypermedia/LinkSnippetResultHandler.java | 32 +-- .../payload/FieldSnippetResultHandler.java | 85 +++--- .../QueryParametersSnippetResultHandler.java | 32 +-- .../restdocs/snippet/AsciidoctorWriter.java | 101 -------- .../restdocs/snippet/DocumentationWriter.java | 125 --------- .../snippet/SnippetWritingResultHandler.java | 5 +- .../StandardTemplateResourceResolver.java | 49 ++++ .../restdocs/templates/Template.java | 22 ++ .../restdocs/templates/TemplateEngine.java | 42 +++ .../templates/TemplateResourceResolver.java | 37 +++ .../templates/mustache/MustacheTemplate.java | 47 ++++ .../mustache/MustacheTemplateEngine.java | 60 +++++ .../templates/default-curl-request.snippet | 4 + .../restdocs/templates/default-fields.snippet | 10 + .../templates/default-http-request.snippet | 8 + .../templates/default-http-response.snippet | 8 + .../restdocs/templates/default-links.snippet | 9 + .../default-query-parameters.snippet | 9 + .../templates/default-request-fields.snippet | 10 + .../templates/default-response-fields.snippet | 10 + .../RestDocumentationIntegrationTests.java | 26 ++ .../snippet/AsciidoctorWriterTests.java | 86 ------- .../restdocs/test/SnippetMatchers.java | 21 +- .../restdocs/test/StubMvcResult.java | 5 + .../restdocs/templates/curl-request.snippet | 1 + 33 files changed, 722 insertions(+), 586 deletions(-) create mode 100644 docs/src/docs/asciidoc/customizing-snippets.adoc delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet create mode 100644 spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet diff --git a/build.gradle b/build.gradle index 3617a140..202910b9 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,7 @@ apply plugin: 'samples' ext { springVersion = '4.1.5.RELEASE' + jmustacheVersion = '1.10' } subprojects { @@ -28,6 +29,7 @@ subprojects { } dependencies { dependency 'com.fasterxml.jackson.core:jackson-databind:2.4.6' + dependency "com.samskivert:jmustache:$jmustacheVersion" dependency 'javax.servlet:javax.servlet-api:3.1.0' dependency 'junit:junit:4.12' dependency 'org.hamcrest:hamcrest-core:1.3' diff --git a/docs/src/docs/asciidoc/customizing-snippets.adoc b/docs/src/docs/asciidoc/customizing-snippets.adoc new file mode 100644 index 00000000..94cca79d --- /dev/null +++ b/docs/src/docs/asciidoc/customizing-snippets.adoc @@ -0,0 +1,12 @@ +[[customizing-snippets]] +== Customizing the generated snippets + +Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the +generated snippets. You can customize the generated snippets by overriding the +{source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates[default +templates]. + +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 +override the template for the `curl-request.adoc` snippet, create a template named +`curl-request.snippet` in `src/test/resources/org/springframework/restdocs/templates`. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 99ec9d5f..f6afcf0e 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -9,7 +9,9 @@ Andy Wilkinson :examples-dir: ../../test/java :github: https://github.com/spring-projects/spring-restdocs -:samples: {github}/tree/{branch-or-tag}/samples +: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 @@ -22,6 +24,7 @@ include::introduction.adoc[] include::getting-started.adoc[] include::documenting-your-api.adoc[] include::customizing-responses.adoc[] +include::customizing-snippets.adoc[] include::configuration.adoc[] include::working-with-asciidoctor.adoc[] include::contributing.adoc[] \ No newline at end of file diff --git a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc index dd0d68d2..ce166410 100644 --- a/docs/src/docs/asciidoc/working-with-asciidoctor.adoc +++ b/docs/src/docs/asciidoc/working-with-asciidoctor.adoc @@ -33,8 +33,8 @@ can be used to reference the snippets output directory, for example: === Customizing tables Many of the snippets contain a table in its default configuration. The appearance of the -table can be customized by providing some additional configuration when the snippet is -included. +table can be customized, either by providing some additional configuration when the +snippet is included or by using a custom snippet template. diff --git a/spring-restdocs/build.gradle b/spring-restdocs/build.gradle index 65056ff4..e4c2fd50 100644 --- a/spring-restdocs/build.gradle +++ b/spring-restdocs/build.gradle @@ -29,6 +29,8 @@ sonarRunner { configurations { jacoco + jarjar + jmustache } ext { @@ -38,6 +40,24 @@ ext { ] as String[] } +task jmustacheRepackJar(type: Jar) { repackJar -> + repackJar.baseName = "restdocs-jmustache-repack" + repackJar.version = jmustacheVersion + + doLast() { + project.ant { + taskdef name: "jarjar", classname: "com.tonicsystems.jarjar.JarJarTask", + classpath: configurations.jarjar.asPath + jarjar(destfile: repackJar.archivePath) { + configurations.jmustache.each { originalJar -> + zipfileset(src: originalJar, includes: '**/*.class') + } + rule(pattern: 'com.samskivert.**', result: 'org.springframework.restdocs.@1') + } + } + } +} + dependencies { compile 'com.fasterxml.jackson.core:jackson-databind' compile 'junit:junit' @@ -46,13 +66,23 @@ dependencies { compile 'org.springframework:spring-web' compile 'org.springframework:spring-webmvc' compile 'javax.servlet:javax.servlet-api' + compile files(jmustacheRepackJar) jacoco 'org.jacoco:org.jacoco.agent::runtime' + jarjar 'com.googlecode.jarjar:jarjar:1.3' + jmustache 'com.samskivert:jmustache@jar' testCompile 'org.springframework.hateoas:spring-hateoas' testCompile 'org.mockito:mockito-core' testCompile 'org.hamcrest:hamcrest-core' testCompile 'org.hamcrest:hamcrest-library' } +jar { + dependsOn jmustacheRepackJar + from(zipTree(jmustacheRepackJar.archivePath)) { + include "org/springframework/restdocs/**" + } +} + task sourcesJar(type: Jar) { classifier = 'sources' from project.sourceSets.main.allSource @@ -60,13 +90,12 @@ task sourcesJar(type: Jar) { javadoc { description = "Generates project-level javadoc for use in -javadoc jar" - options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED options.author = true options.header = "Spring REST Docs $version" options.docTitle = "${options.header} API" options.links(project.ext.javadocLinks) - options.addStringOption('-quiet') + options.addStringOption '-quiet' } task javadocJar(type: Jar) { @@ -80,7 +109,7 @@ artifacts { } test { - jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*" + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=org.springframework.restdocs.*,excludes=org.springframework.restdocs.mustache.*" testLogging { exceptionFormat "full" } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java index 8d578146..c8757d18 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/config/RestDocumentationConfigurer.java @@ -21,6 +21,9 @@ import java.util.List; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.restdocs.RestDocumentation; +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; @@ -44,6 +47,8 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { private final RequestPostProcessor requestPostProcessor; + private TemplateEngineConfigurer templateEngineConfigurer = new TemplateEngineConfigurer(); + /** * Creates a new {@link RestDocumentationConfigurer}. * @see RestDocumentation#documentationConfiguration() @@ -52,7 +57,8 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { this.requestPostProcessor = new ConfigurerApplyingRequestPostProcessor( Arrays. asList(this.uriConfigurer, this.snippetConfigurer, new StepCountConfigurer(), - new ContentLengthHeaderConfigurer())); + new ContentLengthHeaderConfigurer(), + new TemplateEngineConfigurer())); } public UriConfigurer uris() { @@ -63,6 +69,11 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { return this.snippetConfigurer; } + public RestDocumentationConfigurer templateEngine(TemplateEngine templateEngine) { + this.templateEngineConfigurer.setTemplateEngine(templateEngine); + return this; + } + @Override public RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { @@ -95,6 +106,22 @@ public class RestDocumentationConfigurer extends MockMvcConfigurerAdapter { } + private static 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 class ConfigurerApplyingRequestPostProcessor implements RequestPostProcessor { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java index 65368dc6..e3161c52 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/curl/CurlDocumentation.java @@ -17,12 +17,15 @@ package org.springframework.restdocs.curl; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Map.Entry; -import org.springframework.restdocs.snippet.DocumentationWriter; -import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; @@ -50,18 +53,11 @@ public abstract class CurlDocumentation { * @return the handler that will produce the snippet */ public static SnippetWritingResultHandler documentCurlRequest(String outputDir) { - return new SnippetWritingResultHandler(outputDir, "curl-request") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.shellCommand(new CurlRequestDocumentationAction(writer, result)); - } - }; + return new CurlRequestWritingResultHandler(outputDir); } - private static final class CurlRequestDocumentationAction implements - DocumentationAction { + private static final class CurlRequestWritingResultHandler extends + SnippetWritingResultHandler { private static final String SCHEME_HTTP = "http"; @@ -71,46 +67,52 @@ public abstract class CurlDocumentation { private static final int STANDARD_PORT_HTTPS = 443; - private final DocumentationWriter writer; - - private final MvcResult result; - - CurlRequestDocumentationAction(DocumentationWriter writer, MvcResult result) { - this.writer = writer; - this.result = result; + private CurlRequestWritingResultHandler(String outputDir) { + super(outputDir, "curl-request"); } @Override - public void perform() throws IOException { - DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( - this.result.getRequest()); + public void handle(MvcResult result, PrintWriter writer) throws IOException { + Map context = new HashMap(); + context.put("language", "bash"); + context.put("arguments", getCurlCommandArguments(result)); - this.writer.print("curl '"); + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); - writeAuthority(request); - writePathAndQueryString(request); - - this.writer.print("'"); - - writeOptionToIncludeHeadersInOutput(); - writeHttpMethodIfNecessary(request); - writeHeaders(request); - - if (request.isMultipartRequest()) { - writeParts(request); - } - - writeContent(request); - - this.writer.println(); + writer.print(templateEngine.compileTemplate("curl-request").render(context)); } - private void writeAuthority(DocumentableHttpServletRequest request) { - this.writer.print(String.format("%s://%s", request.getScheme(), - request.getHost())); + private String getCurlCommandArguments(MvcResult result) throws IOException { + StringWriter command = new StringWriter(); + PrintWriter printer = new PrintWriter(command); + DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( + result.getRequest()); + + printer.print("'"); + writeAuthority(request, printer); + writePathAndQueryString(request, printer); + printer.print("'"); + + writeOptionToIncludeHeadersInOutput(printer); + writeHttpMethodIfNecessary(request, printer); + writeHeaders(request, printer); + + if (request.isMultipartRequest()) { + writeParts(request, printer); + } + + writeContent(request, printer); + + return command.toString(); + } + + private void writeAuthority(DocumentableHttpServletRequest request, + PrintWriter writer) { + writer.print(String.format("%s://%s", request.getScheme(), request.getHost())); if (isNonStandardPort(request)) { - this.writer.print(String.format(":%d", request.getPort())); + writer.print(String.format(":%d", request.getPort())); } } @@ -119,75 +121,75 @@ public abstract class CurlDocumentation { || (SCHEME_HTTPS.equals(request.getScheme()) && request.getPort() != STANDARD_PORT_HTTPS); } - private void writePathAndQueryString(DocumentableHttpServletRequest request) { + private void writePathAndQueryString(DocumentableHttpServletRequest request, + PrintWriter writer) { if (StringUtils.hasText(request.getContextPath())) { - this.writer.print(String.format( + writer.print(String.format( request.getContextPath().startsWith("/") ? "%s" : "/%s", request.getContextPath())); } - this.writer.print(request.getRequestUriWithQueryString()); + writer.print(request.getRequestUriWithQueryString()); } - private void writeOptionToIncludeHeadersInOutput() { - this.writer.print(" -i"); + private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) { + writer.print(" -i"); } - private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request) { + private void writeHttpMethodIfNecessary(DocumentableHttpServletRequest request, + PrintWriter writer) { if (!request.isGetRequest()) { - this.writer.print(String.format(" -X %s", request.getMethod())); + writer.print(String.format(" -X %s", request.getMethod())); } } - private void writeHeaders(DocumentableHttpServletRequest request) { + private void writeHeaders(DocumentableHttpServletRequest request, + PrintWriter writer) { for (Entry> entry : request.getHeaders().entrySet()) { for (String header : entry.getValue()) { - this.writer.print(String.format(" -H '%s: %s'", entry.getKey(), - header)); + writer.print(String.format(" -H '%s: %s'", entry.getKey(), header)); } } } - private void writeParts(DocumentableHttpServletRequest request) + private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) throws IOException { for (Entry> entry : request.getMultipartFiles() .entrySet()) { for (MultipartFile file : entry.getValue()) { - this.writer.printf(" -F '%s=", file.getName()); + writer.printf(" -F '%s=", file.getName()); if (!StringUtils.hasText(file.getOriginalFilename())) { - this.writer.append(new String(file.getBytes())); + writer.append(new String(file.getBytes())); } else { - this.writer.printf("@%s", file.getOriginalFilename()); + writer.printf("@%s", file.getOriginalFilename()); } if (StringUtils.hasText(file.getContentType())) { - this.writer.append(";type=").append(file.getContentType()); + writer.append(";type=").append(file.getContentType()); } - this.writer.append("'"); + writer.append("'"); } } } - private void writeContent(DocumentableHttpServletRequest request) - throws IOException { + private void writeContent(DocumentableHttpServletRequest request, + PrintWriter writer) throws IOException { if (request.getContentLength() > 0) { - this.writer - .print(String.format(" -d '%s'", request.getContentAsString())); + writer.print(String.format(" -d '%s'", request.getContentAsString())); } else if (request.isMultipartRequest()) { for (Entry entry : request.getParameterMap().entrySet()) { for (String value : entry.getValue()) { - this.writer.print(String.format(" -F '%s=%s'", entry.getKey(), - value)); + writer.print(String.format(" -F '%s=%s'", entry.getKey(), value)); } } } else if (request.isPostRequest() || request.isPutRequest()) { String queryString = request.getParameterMapAsQueryString(); if (StringUtils.hasText(queryString)) { - this.writer.print(String.format(" -d '%s'", queryString)); + writer.print(String.format(" -d '%s'", queryString)); } } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java index 025f17ed..2f7448fd 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/http/HttpDocumentation.java @@ -17,15 +17,19 @@ package org.springframework.restdocs.http; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.restdocs.snippet.DocumentationWriter; -import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.restdocs.util.DocumentableHttpServletRequest; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.StringUtils; @@ -39,6 +43,8 @@ import org.springframework.web.multipart.MultipartFile; */ public abstract class HttpDocumentation { + private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; + private HttpDocumentation() { } @@ -51,15 +57,7 @@ public abstract class HttpDocumentation { * @return the handler that will produce the snippet */ public static SnippetWritingResultHandler documentHttpRequest(String outputDir) { - return new SnippetWritingResultHandler(outputDir, "http-request") { - - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.codeBlock("http", new HttpRequestDocumentationAction(writer, - result)); - } - }; + return new HttpRequestWritingResultHandler(outputDir); } /** @@ -70,71 +68,126 @@ public abstract class HttpDocumentation { * @return the handler that will produce the snippet */ public static SnippetWritingResultHandler documentHttpResponse(String outputDir) { - return new SnippetWritingResultHandler(outputDir, "http-response") { + return new HttpResponseWritingResultHandler(outputDir); - @Override - public void handle(MvcResult result, DocumentationWriter writer) - throws IOException { - writer.codeBlock("http", new HttpResponseDocumentationAction(writer, - result)); - } - }; } - private static class HttpRequestDocumentationAction implements DocumentationAction { + private static final class HttpRequestWritingResultHandler extends + SnippetWritingResultHandler { - private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm"; - - private final DocumentationWriter writer; - - private final MvcResult result; - - HttpRequestDocumentationAction(DocumentationWriter writer, MvcResult result) { - this.writer = writer; - this.result = result; + private HttpRequestWritingResultHandler(String outputDir) { + super(outputDir, "http-request"); } @Override - public void perform() throws IOException { + public void handle(MvcResult result, PrintWriter writer) throws IOException { DocumentableHttpServletRequest request = new DocumentableHttpServletRequest( - this.result.getRequest()); - this.writer.printf("%s %s HTTP/1.1%n", request.getMethod(), - request.getRequestUriWithQueryString()); + result.getRequest()); + Map context = new HashMap(); + context.put("language", "http"); + context.put("method", result.getRequest().getMethod()); + context.put("path", request.getRequestUriWithQueryString()); + context.put("headers", getHeaders(request)); + context.put("requestBody", getRequestBody(request)); + + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + + writer.print(templateEngine.compileTemplate("http-request").render(context)); + } + + private List> getHeaders( + DocumentableHttpServletRequest request) { + List> headers = new ArrayList<>(); if (requiresHostHeader(request)) { - writeHeader(HttpHeaders.HOST, request.getHost()); + headers.add(header(HttpHeaders.HOST, request.getHost())); } + for (Entry> header : request.getHeaders().entrySet()) { for (String value : header.getValue()) { if (header.getKey() == HttpHeaders.CONTENT_TYPE && request.isMultipartRequest()) { - writeHeader(header.getKey(), String.format("%s; boundary=%s", - value, MULTIPART_BOUNDARY)); + headers.add(header(header.getKey(), String.format( + "%s; boundary=%s", value, MULTIPART_BOUNDARY))); } else { - this.writer.printf("%s: %s%n", header.getKey(), value); + headers.add(header(header.getKey(), value)); } } } if (requiresFormEncodingContentType(request)) { - writeHeader(HttpHeaders.CONTENT_TYPE, - MediaType.APPLICATION_FORM_URLENCODED_VALUE); + headers.add(header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_FORM_URLENCODED_VALUE)); } - this.writer.println(); + return headers; + } + + private String getRequestBody(DocumentableHttpServletRequest request) + throws IOException { + StringWriter httpRequest = new StringWriter(); + PrintWriter writer = new PrintWriter(httpRequest); if (request.getContentLength() > 0) { - this.writer.println(request.getContentAsString()); + writer.println(); + writer.print(request.getContentAsString()); } else if (request.isPostRequest() || request.isPutRequest()) { if (request.isMultipartRequest()) { - writeParts(request); + writeParts(request, writer); } else { String queryString = request.getParameterMapAsQueryString(); if (StringUtils.hasText(queryString)) { - this.writer.println(queryString); + writer.println(); + writer.print(queryString); } } } + return httpRequest.toString(); + } + + private void writeParts(DocumentableHttpServletRequest request, PrintWriter writer) + throws IOException { + writer.println(); + for (Entry parameter : request.getParameterMap().entrySet()) { + for (String value : parameter.getValue()) { + writePartBoundary(writer); + writePart(parameter.getKey(), value, null, writer); + writer.println(); + } + } + for (Entry> entry : request.getMultipartFiles() + .entrySet()) { + for (MultipartFile file : entry.getValue()) { + writePartBoundary(writer); + writePart(file, writer); + writer.println(); + } + } + writeMultipartEnd(writer); + } + + private void writePartBoundary(PrintWriter writer) { + writer.printf("--%s%n", MULTIPART_BOUNDARY); + } + + private void writePart(String name, String value, String contentType, + PrintWriter writer) { + writer.printf("Content-Disposition: form-data; name=%s%n", name); + if (StringUtils.hasText(contentType)) { + writer.printf("Content-Type: %s%n", contentType); + } + writer.println(); + writer.print(value); + } + + private void writePart(MultipartFile part, PrintWriter writer) throws IOException { + writePart(part.getName(), new String(part.getBytes()), part.getContentType(), + writer); + } + + private void writeMultipartEnd(PrintWriter writer) { + writer.printf("--%s--", MULTIPART_BOUNDARY); } private boolean requiresHostHeader(DocumentableHttpServletRequest request) { @@ -148,79 +201,55 @@ public abstract class HttpDocumentation { && StringUtils.hasText(request.getParameterMapAsQueryString()); } - private void writeHeader(String name, String value) { - this.writer.printf("%s: %s%n", name, value); - } - - private void writeParts(DocumentableHttpServletRequest request) - throws IOException { - for (Entry parameter : request.getParameterMap().entrySet()) { - for (String value : parameter.getValue()) { - writePartBoundary(); - writePart(parameter.getKey(), value, null); - this.writer.println(); - } - } - for (Entry> entry : request.getMultipartFiles() - .entrySet()) { - for (MultipartFile file : entry.getValue()) { - writePartBoundary(); - writePart(file); - this.writer.println(); - } - } - writeMultipartEnd(); - } - - private void writePartBoundary() { - this.writer.printf("--%s%n", MULTIPART_BOUNDARY); - } - - private void writePart(String name, String value, String contentType) { - this.writer.printf("Content-Disposition: form-data; name=%s%n", name); - if (StringUtils.hasText(contentType)) { - this.writer.printf("Content-Type: %s%n", contentType); - } - this.writer.println(); - this.writer.print(value); - } - - private void writePart(MultipartFile part) throws IOException { - writePart(part.getName(), new String(part.getBytes()), part.getContentType()); - } - - private void writeMultipartEnd() { - this.writer.printf("--%s--%n", MULTIPART_BOUNDARY); + private Map header(String name, String value) { + Map header = new HashMap<>(); + header.put("name", name); + header.put("value", value); + return header; } } - private static final class HttpResponseDocumentationAction implements - DocumentationAction { + private static final class HttpResponseWritingResultHandler extends + SnippetWritingResultHandler { - private final DocumentationWriter writer; - - private final MvcResult result; - - HttpResponseDocumentationAction(DocumentationWriter writer, MvcResult result) { - this.writer = writer; - this.result = result; + private HttpResponseWritingResultHandler(String outputDir) { + super(outputDir, "http-response"); } @Override - public void perform() throws IOException { - HttpStatus status = HttpStatus.valueOf(this.result.getResponse().getStatus()); - this.writer.println(String.format("HTTP/1.1 %d %s", status.value(), - status.getReasonPhrase())); - for (String headerName : this.result.getResponse().getHeaderNames()) { - for (String header : this.result.getResponse().getHeaders(headerName)) { - this.writer.println(String.format("%s: %s", headerName, header)); + public void handle(MvcResult result, PrintWriter writer) throws IOException { + HttpStatus status = HttpStatus.valueOf(result.getResponse().getStatus()); + Map context = new HashMap(); + context.put( + "responseBody", + StringUtils.hasLength(result.getResponse().getContentAsString()) ? String + .format("%n%s", result.getResponse().getContentAsString()) + : ""); + context.put("language", "http"); + context.put("statusCode", status.value()); + context.put("statusReason", status.getReasonPhrase()); + + List> headers = new ArrayList<>(); + context.put("headers", headers); + + for (String headerName : result.getResponse().getHeaderNames()) { + for (String header : result.getResponse().getHeaders(headerName)) { + headers.add(header(headerName, header)); } } - this.writer.println(); - String content = this.result.getResponse().getContentAsString(); - if (StringUtils.hasText(content)) { - this.writer.println(content); - } + + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + + writer.print(templateEngine.compileTemplate("http-response").render(context)); + + } + + private Map header(String name, String value) { + Map header = new HashMap<>(); + header.put("name", name); + header.put("value", value); + return header; } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index 4702930a..6d18c625 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -17,18 +17,17 @@ package org.springframework.restdocs.hypermedia; import java.io.IOException; +import java.io.PrintWriter; +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.snippet.DocumentationWriter; -import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; -import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -61,10 +60,9 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { } @Override - protected void handle(MvcResult result, DocumentationWriter writer) - throws IOException { + protected void handle(MvcResult result, PrintWriter writer) throws IOException { validate(extractLinks(result)); - writeDocumentationSnippet(writer); + writeDocumentationSnippet(result, writer); } private Map> extractLinks(MvcResult result) throws IOException { @@ -111,19 +109,13 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { } } - private void writeDocumentationSnippet(DocumentationWriter writer) throws IOException { - writer.table(new TableAction() { - - @Override - public void perform(TableWriter tableWriter) throws IOException { - tableWriter.headers("Relation", "Description"); - for (Entry entry : LinkSnippetResultHandler.this.descriptorsByRel - .entrySet()) { - tableWriter.row(entry.getKey(), entry.getValue().getDescription()); - } - } - - }); + private void writeDocumentationSnippet(MvcResult result, PrintWriter writer) + throws IOException { + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + Map context = new HashMap<>(); + context.put("links", this.descriptorsByRel.values()); + writer.print(templateEngine.compileTemplate("links").render(context)); } } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java index 84627c42..1e87a551 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -17,16 +17,17 @@ package org.springframework.restdocs.payload; import java.io.IOException; +import java.io.PrintWriter; import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import org.springframework.restdocs.snippet.DocumentationWriter; -import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; -import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -35,7 +36,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; /** * A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful * resource's request or response fields. - * + * * @author Andreas Evers * @author Andy Wilkinson */ @@ -49,11 +50,14 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand private final ObjectMapper objectMapper = new ObjectMapper(); + private final String templateName; + private List fieldDescriptors; - FieldSnippetResultHandler(String outputDir, String filename, + FieldSnippetResultHandler(String outputDir, String type, List descriptors) { - super(outputDir, filename + "-fields"); + super(outputDir, type + "-fields"); + this.templateName = type + "-fields"; for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath()); Assert.hasText(descriptor.getDescription()); @@ -63,49 +67,46 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand } @Override - protected void handle(MvcResult result, DocumentationWriter writer) - throws IOException { + protected void handle(MvcResult result, PrintWriter writer) throws IOException { this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors); - final Object payload = extractPayload(result); + Object payload = extractPayload(result); - writer.table(new TableAction() { - - @Override - public void perform(TableWriter tableWriter) throws IOException { - tableWriter.headers("Path", "Type", "Description"); - for (Entry entry : FieldSnippetResultHandler.this.descriptorsByPath - .entrySet()) { - FieldDescriptor descriptor = entry.getValue(); - FieldType type = getFieldType(descriptor, payload); - tableWriter.row(entry.getKey().toString(), type.toString(), entry - .getValue().getDescription()); - } + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + Map context = new HashMap<>(); + List> fields = new ArrayList<>(); + context.put("fields", fields); + for (Entry entry : this.descriptorsByPath.entrySet()) { + FieldDescriptor descriptor = entry.getValue(); + FieldType type = getFieldType(descriptor, payload); + Map fieldModel = new HashMap<>(); + fieldModel.put("path", entry.getKey()); + fieldModel.put("type", type.toString()); + fieldModel.put("description", descriptor.getDescription()); + fields.add(fieldModel); + } + writer.print(templateEngine.compileTemplate(this.templateName).render(context)); + } + private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { + if (descriptor.getType() != null) { + return descriptor.getType(); + } + else { + try { + return FieldSnippetResultHandler.this.fieldTypeResolver.resolveFieldType( + descriptor.getPath(), payload); } - - private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { - if (descriptor.getType() != null) { - return descriptor.getType(); - } - else { - try { - return FieldSnippetResultHandler.this.fieldTypeResolver - .resolveFieldType(descriptor.getPath(), payload); - } - 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(FieldType)."; - throw new FieldTypeRequiredException(message); - } - } + 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(FieldType)."; + throw new FieldTypeRequiredException(message); } - - }); - + } } private Object extractPayload(MvcResult result) throws IOException { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java index 1058b0a8..8813d0c8 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java @@ -17,17 +17,16 @@ package org.springframework.restdocs.request; import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; -import org.springframework.restdocs.snippet.DocumentationWriter; -import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; -import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; import org.springframework.restdocs.snippet.SnippetGenerationException; import org.springframework.restdocs.snippet.SnippetWritingResultHandler; +import org.springframework.restdocs.templates.TemplateEngine; import org.springframework.test.web.servlet.MvcResult; import org.springframework.util.Assert; @@ -52,10 +51,9 @@ public class QueryParametersSnippetResultHandler extends SnippetWritingResultHan } @Override - protected void handle(MvcResult result, DocumentationWriter writer) - throws IOException { + protected void handle(MvcResult result, PrintWriter writer) throws IOException { verifyParameterDescriptors(result); - documentParameters(writer); + documentParameters(result, writer); } private void verifyParameterDescriptors(MvcResult result) { @@ -87,19 +85,13 @@ public class QueryParametersSnippetResultHandler extends SnippetWritingResultHan Assert.isTrue(actualParameters.equals(expectedParameters)); } - private void documentParameters(DocumentationWriter writer) throws IOException { - writer.table(new TableAction() { - - @Override - public void perform(TableWriter tableWriter) throws IOException { - tableWriter.headers("Parameter", "Description"); - for (Entry entry : QueryParametersSnippetResultHandler.this.descriptorsByName - .entrySet()) { - tableWriter.row(entry.getKey(), entry.getValue().getDescription()); - } - } - - }); + private void documentParameters(MvcResult result, PrintWriter writer) + throws IOException { + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); + Map context = new HashMap<>(); + context.put("parameters", this.descriptorsByName.values()); + writer.print(templateEngine.compileTemplate("query-parameters").render(context)); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java deleted file mode 100644 index fe35ef42..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/AsciidoctorWriter.java +++ /dev/null @@ -1,101 +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.snippet; - -import java.io.IOException; -import java.io.Writer; - -/** - * A {@link DocumentationWriter} that produces output in Asciidoctor. - * - * @author Andy Wilkinson - */ -public class AsciidoctorWriter extends DocumentationWriter { - - private static final String DELIMITER_CODE_BLOCK = "----"; - - private static final String DELIMITER_TABLE = "|==="; - - private final TableWriter tableWriter = new AsciidoctorTableWriter(); - - /** - * Creates a new {@code AsciidoctorWriter} that will write to the given {@code writer} - * @param writer The writer to which output will be written - */ - public AsciidoctorWriter(Writer writer) { - super(writer); - } - - @Override - public void shellCommand(final DocumentationAction action) throws IOException { - codeBlock("bash", new DocumentationAction() { - - @Override - public void perform() throws IOException { - AsciidoctorWriter.this.print("$ "); - action.perform(); - } - }); - } - - @Override - public void codeBlock(String language, DocumentationAction action) throws IOException { - println(); - if (language != null) { - println("[source," + language + "]"); - } - println(DELIMITER_CODE_BLOCK); - action.perform(); - println(DELIMITER_CODE_BLOCK); - println(); - } - - @Override - public void table(TableAction action) throws IOException { - println(); - println(DELIMITER_TABLE); - action.perform(this.tableWriter); - println(DELIMITER_TABLE); - println(); - } - - private final class AsciidoctorTableWriter implements TableWriter { - - @Override - public void headers(String... headers) { - StringBuilder builder = new StringBuilder(); - for (String header : headers) { - builder.append("|"); - builder.append(header); - } - println(builder.toString()); - println(); - } - - @Override - public void row(String... entries) { - for (String entry : entries) { - print("|"); - println(entry); - } - println(); - } - - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java deleted file mode 100644 index b5634401..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/DocumentationWriter.java +++ /dev/null @@ -1,125 +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.snippet; - -import java.io.IOException; -import java.io.PrintWriter; -import java.io.Writer; - -/** - * A {@link PrintWriter} that provides additional methods which are useful for producing - * API documentation. - * - * @author Andy Wilkinson - */ -public abstract class DocumentationWriter extends PrintWriter { - - protected DocumentationWriter(Writer writer) { - super(writer, true); - } - - /** - * Calls the given {@code action} to document a shell command. Any prefix necessary - * for the documentation format is written prior to calling the {@code action}. Having - * called the action, any necessary suffix is then written. - * - * @param action the action that will produce the shell command - * @throws IOException if the documentation fails - */ - public abstract void shellCommand(DocumentationAction action) throws IOException; - - /** - * Calls the given {@code action} to document a code block. The code block will be - * annotated as containing code written in the given {@code language}. Any prefix - * necessary for the documentation format is written prior to calling the - * {@code action}. Having called the {@code action}, any necessary suffix is then - * written. - * - * @param language the language in which the code is written - * @param action the action that will produce the code - * @throws IOException if the documentation fails - */ - public abstract void codeBlock(String language, DocumentationAction action) - throws IOException; - - /** - * Calls the given {@code action} to document a table. Any prefix necessary for - * documenting a table is written prior to calling the {@code action}. Having called - * the {@code action}, any necessary suffix is then written. - * - * @param action the action that will produce the table - * @throws IOException if the documentation fails - */ - public abstract void table(TableAction action) throws IOException; - - /** - * Encapsulates an action that outputs some documentation. Typically implemented as a - * lamda or, pre-Java 8, as an anonymous inner class. - * - * @see DocumentationWriter#shellCommand - * @see DocumentationWriter#codeBlock - */ - public interface DocumentationAction { - - /** - * Perform the encapsulated action - * - * @throws IOException if the action fails - */ - void perform() throws IOException; - - } - - /** - * Encapsulates an action that outputs a table. - * - * @see DocumentationWriter#table(TableAction) - */ - public interface TableAction { - - /** - * Perform the encapsulated action - * - * @param tableWriter the writer to be used to write the table - * @throws IOException if the action fails - */ - void perform(TableWriter tableWriter) throws IOException; - - } - - /** - * A writer for producing a table - */ - public interface TableWriter { - - /** - * Writes the table's headers - * - * @param headers the headers - */ - void headers(String... headers); - - /** - * Writes a row in the table - * - * @param entries the entries in the row - */ - void row(String... entries); - - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java index 1bd057c9..1a1dc022 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/snippet/SnippetWritingResultHandler.java @@ -21,6 +21,7 @@ import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.OutputStreamWriter; +import java.io.PrintWriter; import java.io.Writer; import org.springframework.restdocs.config.RestDocumentationContext; @@ -43,13 +44,13 @@ public abstract class SnippetWritingResultHandler implements ResultHandler { this.fileName = fileName; } - protected abstract void handle(MvcResult result, DocumentationWriter writer) + protected abstract void handle(MvcResult result, PrintWriter writer) throws IOException; @Override public void handle(MvcResult result) throws IOException { try (Writer writer = createWriter()) { - handle(result, new AsciidoctorWriter(writer)); + handle(result, new PrintWriter(writer)); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.java new file mode 100644 index 00000000..eb8fb455 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/StandardTemplateResourceResolver.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.templates; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +/** + * Standard implementation of {@link TemplateResourceResolver}. + *

+ * Templates are resolved by first 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. + * + * @author Andy Wilkinson + */ +public class StandardTemplateResourceResolver implements TemplateResourceResolver { + + @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"); + if (!classPathResource.exists()) { + throw new IllegalStateException("Template named '" + name + + "' could not be resolved"); + } + } + return classPathResource; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java new file mode 100644 index 00000000..302e5f6c --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/Template.java @@ -0,0 +1,22 @@ +package org.springframework.restdocs.templates; + +import java.util.Map; + +/** + * A compiled {@code Template} that can be rendered to a {@link String}. + * + * @author Andy Wilkinson + * + */ +public interface Template { + + /** + * Renders the template to a {@link String} using the given {@code context} for + * variable/property resolution. + * + * @param context The context to use + * @return The rendered template + */ + String render(Map context); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java new file mode 100644 index 00000000..2ab90571 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateEngine.java @@ -0,0 +1,42 @@ +/* + * 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.templates; + +import java.io.IOException; + +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; + +/** + * A {@code TemplateEngine} is used to render documentation snippets. + * + * @author Andy Wilkinson + * @see MustacheTemplateEngine + */ +public interface TemplateEngine { + + /** + * Compiles the template at the given {@code path}. Typically, a + * {@link TemplateResourceResolver} will be used to resolve the path into a resource + * that can be read and compiled. + * + * @param path the path of the template + * @return the compiled {@code Template} + * @throws IOException if compilation fails + */ + Template compileTemplate(String path) throws IOException; + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java new file mode 100644 index 00000000..d4259af1 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/TemplateResourceResolver.java @@ -0,0 +1,37 @@ +/* + * 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.templates; + +import org.springframework.core.io.Resource; + +/** + * A {@code TemplateResourceResolver} is responsible for resolving a name for a template + * into a {@link Resource} from which the template can be read. + * + * @author Andy Wilkinson + */ +public interface TemplateResourceResolver { + + /** + * Resolves a {@link Resource} for the template with the given {@code name}. + * + * @param name the name of the template + * @return the {@code Resource} from which the template can be read + */ + public Resource resolveTemplateResource(String name); + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java new file mode 100644 index 00000000..52421732 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplate.java @@ -0,0 +1,47 @@ +/* + * 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.templates.mustache; + +import java.util.Map; + +import org.springframework.restdocs.templates.Template; + +/** + * An adapter that exposes a compiled Mustache + * template as a {@link Template}. + * + * @author Andy Wilkinson + */ +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. + * @param delegate The delegate to adapt + */ + public MustacheTemplate(org.springframework.restdocs.mustache.Template delegate) { + this.delegate = delegate; + } + + @Override + public String render(Map context) { + return this.delegate.execute(context); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.java new file mode 100644 index 00000000..0b16ae81 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/templates/mustache/MustacheTemplateEngine.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.templates.mustache; + +import java.io.IOException; +import java.io.InputStreamReader; + +import org.springframework.core.io.Resource; +import org.springframework.restdocs.mustache.Mustache; +import org.springframework.restdocs.mustache.Mustache.Compiler; +import org.springframework.restdocs.templates.Template; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; + +/** + * A Mustache-based {@link TemplateEngine} + * implemented using JMustache. + *

+ * Note that JMustache has been repackaged and embedded to prevent classpath conflicts. + * + * @author Andy Wilkinson + */ +public class MustacheTemplateEngine implements TemplateEngine { + + private final Compiler compiler = Mustache.compiler().escapeHTML(false); + + private final TemplateResourceResolver templateResourceResolver; + + /** + * Creates a new {@link MustacheTemplateEngine} that will use the given + * {@code templateResourceResolver} to resolve template paths. + * + * @param templateResourceResolver The resolve to use + */ + public MustacheTemplateEngine(TemplateResourceResolver templateResourceResolver) { + this.templateResourceResolver = templateResourceResolver; + } + + @Override + public Template compileTemplate(String name) throws IOException { + Resource templateResource = this.templateResourceResolver + .resolveTemplateResource(name); + return new MustacheTemplate(this.compiler.compile(new InputStreamReader( + templateResource.getInputStream()))); + } + +} diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet new file mode 100644 index 00000000..8eaef9e1 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-curl-request.snippet @@ -0,0 +1,4 @@ +[source,bash] +---- +$ curl {{arguments}} +---- \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet new file mode 100644 index 00000000..497f5a03 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet new file mode 100644 index 00000000..a3e529ae --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-request.snippet @@ -0,0 +1,8 @@ +[source,http] +---- +{{method}} {{path}} HTTP/1.1 +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{requestBody}} +---- \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet new file mode 100644 index 00000000..fe095d36 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-http-response.snippet @@ -0,0 +1,8 @@ +[source,http] +---- +HTTP/1.1 {{statusCode}} {{statusReason}} +{{#headers}} +{{name}}: {{value}} +{{/headers}} +{{responseBody}} +---- \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet new file mode 100644 index 00000000..b8b45290 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-links.snippet @@ -0,0 +1,9 @@ +|=== +|Relation|Description + +{{#links}} +|{{rel}} +|{{description}} + +{{/links}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet new file mode 100644 index 00000000..067e1fb8 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-query-parameters.snippet @@ -0,0 +1,9 @@ +|=== +|Parameter|Description + +{{#parameters}} +|{{name}} +|{{description}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet new file mode 100644 index 00000000..497f5a03 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-request-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet new file mode 100644 index 00000000..497f5a03 --- /dev/null +++ b/spring-restdocs/src/main/resources/org/springframework/restdocs/templates/default-response-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java index a7dc2b94..e5aa52a6 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/RestDocumentationIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.restdocs; +import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -31,6 +32,8 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -175,6 +178,29 @@ public class RestDocumentationIntegrationTests { + " \"...\"%n } ]%n}"))))); } + @Test + public void customSnippetTemplate() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(new RestDocumentationConfigurer()).build(); + + 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 { + mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("custom-snippet-template")); + } + 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) { assertTrue(new File(directory, snippet).isFile()); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java deleted file mode 100644 index 15eb2728..00000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/snippet/AsciidoctorWriterTests.java +++ /dev/null @@ -1,86 +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.snippet; - -import static org.junit.Assert.assertEquals; - -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; - -import org.junit.Test; -import org.springframework.restdocs.snippet.DocumentationWriter.DocumentationAction; -import org.springframework.restdocs.snippet.DocumentationWriter.TableAction; -import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter; - -/** - * Tests for {@link AsciidoctorWriter} - * - * @author Andy Wilkinson - */ -public class AsciidoctorWriterTests { - - private Writer output = new StringWriter(); - - private DocumentationWriter documentationWriter = new AsciidoctorWriter(this.output); - - @Test - public void codeBlock() throws Exception { - this.documentationWriter.codeBlock("java", new DocumentationAction() { - - @Override - public void perform() throws IOException { - AsciidoctorWriterTests.this.documentationWriter.println("foo"); - } - }); - - String expectedOutput = String.format("%n[source,java]%n----%nfoo%n----%n%n"); - assertEquals(expectedOutput, this.output.toString()); - } - - @Test - public void shellCommand() throws Exception { - this.documentationWriter.shellCommand(new DocumentationAction() { - - @Override - public void perform() throws IOException { - AsciidoctorWriterTests.this.documentationWriter.println("foo"); - } - }); - - String expectedOutput = String.format("%n[source,bash]%n----%n$ foo%n----%n%n"); - assertEquals(expectedOutput, this.output.toString()); - } - - @Test - public void table() throws Exception { - this.documentationWriter.table(new TableAction() { - - @Override - public void perform(TableWriter tableWriter) throws IOException { - tableWriter.headers("One", "Two", "Three"); - tableWriter.row("alpha", "bravo", "charlie"); - tableWriter.row("foo", "bar", "baz"); - } - - }); - String expectedOutput = String - .format("%n|===%n|One|Two|Three%n%n|alpha%n|bravo%n|charlie%n%n|foo%n|bar%n|baz%n%n|===%n%n"); - assertEquals(expectedOutput, this.output.toString()); - System.out.println(this.output.toString()); - } -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java index a8cdb4d0..46eecfcd 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/SnippetMatchers.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; import java.util.List; import org.hamcrest.BaseMatcher; @@ -100,8 +101,12 @@ public class SnippetMatchers { private String getLinesAsString() { StringWriter writer = new StringWriter(); - for (String line : this.lines) { - writer.append(String.format("%s%n", line)); + Iterator iterator = this.lines.iterator(); + while (iterator.hasNext()) { + writer.append(String.format("%s", iterator.next())); + if (iterator.hasNext()) { + writer.append(String.format("%n")); + } } return writer.toString(); } @@ -111,16 +116,14 @@ public class SnippetMatchers { extends AbstractSnippetContentMatcher { protected AsciidoctorCodeBlockMatcher(String language) { - this.addLine(""); this.addLine("[source," + language + "]"); this.addLine("----"); this.addLine("----"); - this.addLine(""); } @SuppressWarnings("unchecked") public T content(String content) { - this.addLine(-2, content); + this.addLine(-1, content); return (T) this; } @@ -129,7 +132,7 @@ public class SnippetMatchers { public static abstract class HttpMatcher> extends AsciidoctorCodeBlockMatcher> { - private int headerOffset = 4; + private int headerOffset = 3; protected HttpMatcher() { super("http"); @@ -164,7 +167,6 @@ public class SnippetMatchers { public static class AsciidoctorTableMatcher extends AbstractSnippetContentMatcher { private AsciidoctorTableMatcher(String... columns) { - this.addLine(""); this.addLine("|==="); String header = "|" + StringUtils @@ -172,14 +174,13 @@ public class SnippetMatchers { this.addLine(header); this.addLine(""); this.addLine("|==="); - this.addLine(""); } public AsciidoctorTableMatcher row(String... entries) { for (String entry : entries) { - this.addLine(-2, "|" + entry); + this.addLine(-1, "|" + entry); } - this.addLine(-2, ""); + this.addLine(-1, ""); return this; } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index 1d4d4651..39ef530d 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -19,6 +19,9 @@ package org.springframework.restdocs.test; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; +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.MvcResult; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.web.servlet.FlashMap; @@ -76,6 +79,8 @@ public class StubMvcResult implements MvcResult { private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { this.request = request; + this.request.setAttribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(new StandardTemplateResourceResolver())); this.response = response; } diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/org/springframework/restdocs/templates/curl-request.snippet new file mode 100644 index 00000000..07f3a48f --- /dev/null +++ b/spring-restdocs/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