Use a template engine to produce the documentation snippets

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.
This commit is contained in:
Andy Wilkinson
2015-07-21 15:38:39 +01:00
parent 6f4fe4ea64
commit 4f850cddd9
33 changed files with 722 additions and 586 deletions

View File

@@ -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'

View File

@@ -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`.

View File

@@ -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[]

View File

@@ -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.

View File

@@ -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"
}

View File

@@ -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.<AbstractConfigurer> 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 {

View File

@@ -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<String, Object> context = new HashMap<String, Object>();
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<String, List<String>> 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<String, List<MultipartFile>> 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<String, String[]> 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));
}
}
}

View File

@@ -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<String, Object> context = new HashMap<String, Object>();
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<Map<String, String>> getHeaders(
DocumentableHttpServletRequest request) {
List<Map<String, String>> headers = new ArrayList<>();
if (requiresHostHeader(request)) {
writeHeader(HttpHeaders.HOST, request.getHost());
headers.add(header(HttpHeaders.HOST, request.getHost()));
}
for (Entry<String, List<String>> header : request.getHeaders().entrySet()) {
for (String value : header.getValue()) {
if (header.getKey() == HttpHeaders.CONTENT_TYPE
&& request.isMultipartRequest()) {
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<String, String[]> parameter : request.getParameterMap().entrySet()) {
for (String value : parameter.getValue()) {
writePartBoundary(writer);
writePart(parameter.getKey(), value, null, writer);
writer.println();
}
}
for (Entry<String, List<MultipartFile>> entry : request.getMultipartFiles()
.entrySet()) {
for (MultipartFile file : entry.getValue()) {
writePartBoundary(writer);
writePart(file, writer);
writer.println();
}
}
writeMultipartEnd(writer);
}
private void writePartBoundary(PrintWriter writer) {
writer.printf("--%s%n", MULTIPART_BOUNDARY);
}
private void writePart(String name, String value, String contentType,
PrintWriter writer) {
writer.printf("Content-Disposition: form-data; name=%s%n", name);
if (StringUtils.hasText(contentType)) {
writer.printf("Content-Type: %s%n", contentType);
}
writer.println();
writer.print(value);
}
private void writePart(MultipartFile part, PrintWriter writer) throws IOException {
writePart(part.getName(), new String(part.getBytes()), part.getContentType(),
writer);
}
private void writeMultipartEnd(PrintWriter writer) {
writer.printf("--%s--", MULTIPART_BOUNDARY);
}
private boolean requiresHostHeader(DocumentableHttpServletRequest request) {
@@ -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<String, String[]> parameter : request.getParameterMap().entrySet()) {
for (String value : parameter.getValue()) {
writePartBoundary();
writePart(parameter.getKey(), value, null);
this.writer.println();
}
}
for (Entry<String, List<MultipartFile>> 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<String, String> header(String name, String value) {
Map<String, String> header = new HashMap<>();
header.put("name", name);
header.put("value", value);
return header;
}
}
private static final class 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<String, Object> context = new HashMap<String, Object>();
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<Map<String, String>> 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<String, String> header(String name, String value) {
Map<String, String> header = new HashMap<>();
header.put("name", name);
header.put("value", value);
return header;
}
}

View File

@@ -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<String, List<Link>> 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<String, LinkDescriptor> 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<String, Object> context = new HashMap<>();
context.put("links", this.descriptorsByRel.values());
writer.print(templateEngine.compileTemplate("links").render(context));
}
}

View File

@@ -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<FieldDescriptor> fieldDescriptors;
FieldSnippetResultHandler(String outputDir, String filename,
FieldSnippetResultHandler(String outputDir, String type,
List<FieldDescriptor> 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<String, FieldDescriptor> 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<String, Object> context = new HashMap<>();
List<Map<String, String>> fields = new ArrayList<>();
context.put("fields", fields);
for (Entry<String, FieldDescriptor> entry : this.descriptorsByPath.entrySet()) {
FieldDescriptor descriptor = entry.getValue();
FieldType type = getFieldType(descriptor, payload);
Map<String, String> 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 {

View File

@@ -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<String, ParameterDescriptor> 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<String, Object> context = new HashMap<>();
context.put("parameters", this.descriptorsByName.values());
writer.print(templateEngine.compileTemplate("query-parameters").render(context));
}
}

View File

@@ -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 <a
* href="http://asciidoctor.org">Asciidoctor</a>.
*
* @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();
}
}
}

View File

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

View File

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

View File

@@ -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}.
* <p>
* Templates are resolved by first looking for a resource on the classpath named
* {@code org/springframework/restdocs/templates/&#123;name&#125;.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;
}
}

View File

@@ -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<String, Object> context);
}

View File

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

View File

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

View File

@@ -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 <a href="https://mustache.github.io">Mustache</a>
* 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<String, Object> context) {
return this.delegate.execute(context);
}
}

View File

@@ -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 <a href="https://mustache.github.io">Mustache</a>-based {@link TemplateEngine}
* implemented using <a href="https://github.com/samskivert/jmustache">JMustache</a>.
* <p>
* 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())));
}
}

View File

@@ -0,0 +1,4 @@
[source,bash]
----
$ curl {{arguments}}
----

View File

@@ -0,0 +1,10 @@
|===
|Path|Type|Description
{{#fields}}
|{{path}}
|{{type}}
|{{description}}
{{/fields}}
|===

View File

@@ -0,0 +1,8 @@
[source,http]
----
{{method}} {{path}} HTTP/1.1
{{#headers}}
{{name}}: {{value}}
{{/headers}}
{{requestBody}}
----

View File

@@ -0,0 +1,8 @@
[source,http]
----
HTTP/1.1 {{statusCode}} {{statusReason}}
{{#headers}}
{{name}}: {{value}}
{{/headers}}
{{responseBody}}
----

View File

@@ -0,0 +1,9 @@
|===
|Relation|Description
{{#links}}
|{{rel}}
|{{description}}
{{/links}}
|===

View File

@@ -0,0 +1,9 @@
|===
|Parameter|Description
{{#parameters}}
|{{name}}
|{{description}}
{{/parameters}}
|===

View File

@@ -0,0 +1,10 @@
|===
|Path|Type|Description
{{#fields}}
|{{path}}
|{{type}}
|{{description}}
{{/fields}}
|===

View File

@@ -0,0 +1,10 @@
|===
|Path|Type|Description
{{#fields}}
|{{path}}
|{{type}}
|{{description}}
{{/fields}}
|===

View File

@@ -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());

View File

@@ -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());
}
}

View File

@@ -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<String> 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<T extends HttpMatcher<T>> extends
AsciidoctorCodeBlockMatcher<HttpMatcher<T>> {
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;
}
}

View File

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