Allow documentation of extra attributes that apply to the whole snippet

This commit adds support for associating custom attributes with the
generation of a particular snippet. The attributes are included in
the model during snippet rendering allowing them to be referenced from
a custom snippet template. Among other things, this makes it possible
to provide a configurable title for snippets that produce a code
block.

Closes gh-77
This commit is contained in:
Andy Wilkinson
2015-07-27 15:39:01 +01:00
parent 70824aa509
commit 02c6c89172
28 changed files with 727 additions and 159 deletions

View File

@@ -283,39 +283,52 @@ override the template for the `curl-request.adoc` snippet, create a template nam
[[documenting-your-api-customizing-including-extra-information]]
==== Including extra information
The descriptors for fields, links, and query parameters all have an `attribute` method
that can be used to associate one or more key-value pairs with the descriptor. These
attributes are made available during the template rendering process. Coupled with
a custom snippet template, this makes it possible to include extra information in a
generated snippet.
A concrete example of the above is the addition of a constraints column when documenting
request fields. The first step is to provide a `constraints` attribute for each field that
you are documenting:
There are two ways to provide extra information for inclusion in a generated snippet:
. Use the `attributes` method on a field, link or query parameter descriptor to add one or
more attributes to an individual descriptor
. Pass in some attributes when calling `withCurlRequest`, `withHttpRequest`,
`withHttpResponse`, etc on `RestDocumentationResultHandler`. Such attributes will be
associated with the snippet as a whole.
Any additional attributes are made available during the template rendering process.
Coupled with a custom snippet template, this makes it possible to include extra
information in a generated snippet.
A concrete example of the above is the addition of a constraints column and a title when
documenting request fields. The first step is to provide a `constraints` attribute for
each field that you are documenting and to provide a `title` attribute:
[source,java,indent=0]
----
include::{examples-dir}/com/example/Payload.java[tags=constraints]
----
<1> Configure the `title` attribute for the request fields snippet
<2> Set the `constraints` attribute for the `name` field
<3> Set the `constraints` attribute for the `email` field
The second step is to provide a custom template named `request-fields.snippet` that
includes the information about the fields' constraints in the generated snippet's table:
includes the information about the fields' constraints in the generated snippet's table
and adds a title:
[source,indent=0]
----
.{{title}} <1>
|===
|Path|Type|Description|Constraints <1>
|Path|Type|Description|Constraints <2>
{{#fields}}
|{{path}}
|{{type}}
|{{description}}
|{{constraints}} <2>
|{{constraints}} <3>
{{/fields}}
|===
----
<1> Add a new column named "Constraints"
<2> Include the descriptors' `constraints` attribute in each row of the table
<1> Add a title to the table
<2> Add a new column named "Constraints"
<3> Include the descriptors' `constraints` attribute in each row of the table

View File

@@ -17,12 +17,16 @@
package com.example;
import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.Attributes.attributes;
import static org.springframework.restdocs.Attributes.key;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.restdocs.Attributes.key;
import org.springframework.http.MediaType;
import org.springframework.restdocs.Attributes;
import org.springframework.restdocs.payload.FieldType;
import org.springframework.test.web.servlet.MockMvc;
@@ -57,12 +61,16 @@ private MockMvc mockMvc;
.andExpect(status().isOk())
// tag::constraints[]
.andDo(document("create-user").withRequestFields(
attributes(
key("title").value("Fields for user creation")), // <1>
fieldWithPath("name")
.description("The user's name")
.attribute("constraints", "Must not be null. Must not be empty"),
.attributes(
key("constraints").value("Must not be null. Must not be empty")), // <2>
fieldWithPath("email")
.description("The user's email address")
.attribute("constrains", "Must be a valid email address")));
.attributes(
key("constraints").value("Must be a valid email address")))); // <3>
// end::constraints[]
}

View File

@@ -3,6 +3,8 @@ package org.springframework.restdocs;
import java.util.HashMap;
import java.util.Map;
import org.springframework.restdocs.Attributes.Attribute;
/**
* Base class for descriptors. Provides the ability to associate arbitrary attributes with
* a descriptor.
@@ -13,17 +15,19 @@ import java.util.Map;
*/
public abstract class AbstractDescriptor<T extends AbstractDescriptor<T>> {
private final Map<String, Object> attributes = new HashMap<>();
private Map<String, Object> attributes = new HashMap<>();
/**
* Sets an attribute with the given {@code name} and {@code value}.
* Sets the descriptor's attributes
*
* @param name The name of the attribute
* @param value The value of the attribute
* @param attributes the attributes
* @return the descriptor
*/
@SuppressWarnings("unchecked")
public T attribute(String name, Object value) {
this.attributes.put(name, value);
public T attributes(Attribute... attributes) {
for (Attribute attribute : attributes) {
this.attributes.put(attribute.getKey(), attribute.getValue());
}
return (T) this;
}

View File

@@ -0,0 +1,105 @@
/*
* 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;
import java.util.HashMap;
import java.util.Map;
/**
* A fluent API for building a map of attributes
*
* @author Andy Wilkinson
*/
public abstract class Attributes {
private Attributes() {
}
/**
* Creates an attribute with the given {@code key}. A value for the attribute must
* still be specified.
*
* @param key The key of the attribute
* @return An {@code AttributeBuilder} to use to specify the value of the attribute
* @see AttributeBuilder#value(Object)
*/
public static AttributeBuilder key(String key) {
return new AttributeBuilder(key);
}
/**
* Creates a {@code Map} of the given {@code attributes}.
*
* @param attributes The attributes
* @return A Map of the attributes
*/
public static Map<String, Object> attributes(Attribute... attributes) {
Map<String, Object> attributeMap = new HashMap<>();
for (Attribute attribute : attributes) {
attributeMap.put(attribute.getKey(), attribute.getValue());
}
return attributeMap;
}
/**
* A simple builder for an attribute (key-value pair)
*/
public static class AttributeBuilder {
private final String key;
private AttributeBuilder(String key) {
this.key = key;
}
/**
* Configures the value of the attribute
*
* @param value The attribute's value
* @return A newly created {@code Attribute}
*/
public Attribute value(Object value) {
return new Attribute(this.key, value);
}
}
/**
* An attribute (key-value pair).
*
* @author Andy Wilkinson
*/
public static class Attribute {
private final String key;
private final Object value;
public Attribute(String key, Object value) {
this.key = key;
this.value = value;
}
public String getKey() {
return this.key;
}
public Object getValue() {
return this.value;
}
}
}

View File

@@ -26,6 +26,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.document
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.restdocs.hypermedia.HypermediaDocumentation;
import org.springframework.restdocs.hypermedia.LinkDescriptor;
@@ -35,6 +36,7 @@ import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.PayloadDocumentation;
import org.springframework.restdocs.request.ParameterDescriptor;
import org.springframework.restdocs.request.RequestDocumentation;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
@@ -49,15 +51,55 @@ public class RestDocumentationResultHandler implements ResultHandler {
private final String outputDir;
private List<ResultHandler> delegates = new ArrayList<>();
private SnippetWritingResultHandler curlRequest;
private SnippetWritingResultHandler httpRequest;
private SnippetWritingResultHandler httpResponse;
private List<SnippetWritingResultHandler> delegates = new ArrayList<>();
RestDocumentationResultHandler(String outputDir) {
this.outputDir = outputDir;
this.curlRequest = documentCurlRequest(this.outputDir, null);
this.httpRequest = documentHttpRequest(this.outputDir, null);
this.httpResponse = documentHttpResponse(this.outputDir, null);
}
this.delegates = new ArrayList<ResultHandler>();
this.delegates.add(documentCurlRequest(this.outputDir));
this.delegates.add(documentHttpRequest(this.outputDir));
this.delegates.add(documentHttpResponse(this.outputDir));
/**
* Customizes the default curl request snippet generation to make the given attributes
* available.
*
* @param attributes the attributes
* @return {@code this}
*/
public RestDocumentationResultHandler withCurlRequest(Map<String, Object> attributes) {
this.curlRequest = documentCurlRequest(this.outputDir, attributes);
return this;
}
/**
* Customizes the default HTTP request snippet generation to make the given attributes
* available.
*
* @param attributes the attributes
* @return {@code this}
*/
public RestDocumentationResultHandler withHttpRequest(Map<String, Object> attributes) {
this.httpRequest = documentHttpRequest(this.outputDir, attributes);
return this;
}
/**
* Customizes the default HTTP response snippet generation to make the given
* attributes available.
*
* @param attributes the attributes
* @return {@code this}
*/
public RestDocumentationResultHandler withHttpResponse(Map<String, Object> attributes) {
this.httpResponse = documentHttpResponse(this.outputDir, attributes);
return this;
}
/**
@@ -75,7 +117,7 @@ public class RestDocumentationResultHandler implements ResultHandler {
* @see LinkExtractors#extractorForContentType(String)
*/
public RestDocumentationResultHandler withLinks(LinkDescriptor... descriptors) {
return withLinks(null, descriptors);
return withLinks(null, null, descriptors);
}
/**
@@ -94,7 +136,50 @@ public class RestDocumentationResultHandler implements ResultHandler {
*/
public RestDocumentationResultHandler withLinks(LinkExtractor linkExtractor,
LinkDescriptor... descriptors) {
this.delegates.add(documentLinks(this.outputDir, linkExtractor, descriptors));
return this.withLinks(null, linkExtractor, descriptors);
}
/**
* Document the links in the response using the given {@code descriptors}. The links
* are extracted from the response based on its content type. The given
* {@code attributes} are made available during the generation of the links snippet.
* <p>
* If a link is present in the response but is not described by one of the descriptors
* a failure will occur when this handler is invoked. Similarly, if a link is
* described but is not present in the response a failure will also occur when this
* handler is invoked.
*
* @param attributes the attributes
* @param descriptors the link descriptors
* @return {@code this}
* @see HypermediaDocumentation#linkWithRel(String)
* @see LinkExtractors#extractorForContentType(String)
*/
public RestDocumentationResultHandler withLinks(Map<String, Object> attributes,
LinkDescriptor... descriptors) {
return withLinks(attributes, null, descriptors);
}
/**
* Document the links in the response using the given {@code descriptors}. The links
* are extracted from the response using the given {@code linkExtractor}. The given
* {@code attributes} are made available during the generation of the links snippet.
* <p>
* If a link is present in the response but is not described by one of the descriptors
* a failure will occur when this handler is invoked. Similarly, if a link is
* described but is not present in the response a failure will also occur when this
* handler is invoked.
*
* @param attributes the attributes
* @param linkExtractor used to extract the links from the response
* @param descriptors the link descriptors
* @return {@code this}
* @see HypermediaDocumentation#linkWithRel(String)
*/
public RestDocumentationResultHandler withLinks(Map<String, Object> attributes,
LinkExtractor linkExtractor, LinkDescriptor... descriptors) {
this.delegates.add(documentLinks(this.outputDir, attributes, linkExtractor,
descriptors));
return this;
}
@@ -114,7 +199,30 @@ public class RestDocumentationResultHandler implements ResultHandler {
*/
public RestDocumentationResultHandler withRequestFields(
FieldDescriptor... descriptors) {
this.delegates.add(documentRequestFields(this.outputDir, descriptors));
return this.withRequestFields(null, descriptors);
}
/**
* Document the fields in the request using the given {@code descriptors}. The given
* {@code attributes} are made available during the generation of the request fields
* snippet.
* <p>
* If a field is present in the request but is not documented by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the request a
* failure will also occur. For payloads with a hierarchical structure, documenting a
* field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param descriptors the link descriptors
* @param attributes the attributes
* @return {@code this}
* @see PayloadDocumentation#fieldWithPath(String)
*/
public RestDocumentationResultHandler withRequestFields(
Map<String, Object> attributes, FieldDescriptor... descriptors) {
this.delegates
.add(documentRequestFields(this.outputDir, attributes, descriptors));
return this;
}
@@ -134,7 +242,30 @@ public class RestDocumentationResultHandler implements ResultHandler {
*/
public RestDocumentationResultHandler withResponseFields(
FieldDescriptor... descriptors) {
this.delegates.add(documentResponseFields(this.outputDir, descriptors));
return this.withResponseFields(null, descriptors);
}
/**
* Document the fields in the response using the given {@code descriptors}. The given
* {@code attributes} are made available during the generation of the request fields
* snippet.
* <p>
* If a field is present in the response but is not documented by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* field is documented, is not marked as optional, and is not present in the response
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field is sufficient for all of its descendants to also be treated as having been
* documented.
*
* @param descriptors the link descriptors
* @param attributes the attributes
* @return {@code this}
* @see PayloadDocumentation#fieldWithPath(String)
*/
public RestDocumentationResultHandler withResponseFields(
Map<String, Object> attributes, FieldDescriptor... descriptors) {
this.delegates
.add(documentResponseFields(this.outputDir, attributes, descriptors));
return this;
}
@@ -153,12 +284,36 @@ public class RestDocumentationResultHandler implements ResultHandler {
*/
public RestDocumentationResultHandler withQueryParameters(
ParameterDescriptor... descriptors) {
this.delegates.add(documentQueryParameters(this.outputDir, descriptors));
return this.withQueryParameters(null, descriptors);
}
/**
* Documents the parameters in the request's query string using the given
* {@code descriptors}. The given {@code attributes} are made available during the
* generation of the query parameters snippet.
* <p>
* If a parameter is present in the query string but is not described by one of the
* descriptors a failure will occur when this handler is invoked. Similarly, if a
* parameter is described but is not present in the request a failure will also occur
* when this handler is invoked.
*
* @param descriptors the parameter descriptors
* @param attributes the attributes
* @return {@code this}
* @see RequestDocumentation#parameterWithName(String)
*/
public RestDocumentationResultHandler withQueryParameters(
Map<String, Object> attributes, ParameterDescriptor... descriptors) {
this.delegates.add(documentQueryParameters(this.outputDir, attributes,
descriptors));
return this;
}
@Override
public void handle(MvcResult result) throws Exception {
this.curlRequest.handle(result);
this.httpRequest.handle(result);
this.httpResponse.handle(result);
for (ResultHandler delegate : this.delegates) {
delegate.handle(result);
}

View File

@@ -50,10 +50,13 @@ public abstract class CurlDocumentation {
* Produces a documentation snippet containing the request formatted as a cURL command
*
* @param outputDir The directory to which snippet should be written
* @param attributes Attributes made available during rendering of the curl request
* snippet
* @return the handler that will produce the snippet
*/
public static SnippetWritingResultHandler documentCurlRequest(String outputDir) {
return new CurlRequestWritingResultHandler(outputDir);
public static SnippetWritingResultHandler documentCurlRequest(String outputDir,
Map<String, Object> attributes) {
return new CurlRequestWritingResultHandler(outputDir, attributes);
}
private static final class CurlRequestWritingResultHandler extends
@@ -67,15 +70,16 @@ public abstract class CurlDocumentation {
private static final int STANDARD_PORT_HTTPS = 443;
private CurlRequestWritingResultHandler(String outputDir) {
super(outputDir, "curl-request");
private CurlRequestWritingResultHandler(String outputDir,
Map<String, Object> attributes) {
super(outputDir, "curl-request", attributes);
}
@Override
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));
context.putAll(getAttributes());
TemplateEngine templateEngine = (TemplateEngine) result.getRequest()
.getAttribute(TemplateEngine.class.getName());

View File

@@ -54,10 +54,13 @@ public abstract class HttpDocumentation {
* request
*
* @param outputDir The directory to which snippet should be written
* @param attributes Attributes made available during rendering of the HTTP requst
* snippet
* @return the handler that will produce the snippet
*/
public static SnippetWritingResultHandler documentHttpRequest(String outputDir) {
return new HttpRequestWritingResultHandler(outputDir);
public static SnippetWritingResultHandler documentHttpRequest(String outputDir,
Map<String, Object> attributes) {
return new HttpRequestWritingResultHandler(outputDir, attributes);
}
/**
@@ -65,18 +68,22 @@ public abstract class HttpDocumentation {
* response sent by the server
*
* @param outputDir The directory to which snippet should be written
* @param attributes Attributes made available during rendering of the HTTP response
* snippet
* @return the handler that will produce the snippet
*/
public static SnippetWritingResultHandler documentHttpResponse(String outputDir) {
return new HttpResponseWritingResultHandler(outputDir);
public static SnippetWritingResultHandler documentHttpResponse(String outputDir,
Map<String, Object> attributes) {
return new HttpResponseWritingResultHandler(outputDir, attributes);
}
private static final class HttpRequestWritingResultHandler extends
SnippetWritingResultHandler {
private HttpRequestWritingResultHandler(String outputDir) {
super(outputDir, "http-request");
private HttpRequestWritingResultHandler(String outputDir,
Map<String, Object> attributes) {
super(outputDir, "http-request", attributes);
}
@Override
@@ -84,11 +91,11 @@ public abstract class HttpDocumentation {
DocumentableHttpServletRequest request = new DocumentableHttpServletRequest(
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));
context.putAll(getAttributes());
TemplateEngine templateEngine = (TemplateEngine) result.getRequest()
.getAttribute(TemplateEngine.class.getName());
@@ -212,8 +219,9 @@ public abstract class HttpDocumentation {
private static final class HttpResponseWritingResultHandler extends
SnippetWritingResultHandler {
private HttpResponseWritingResultHandler(String outputDir) {
super(outputDir, "http-response");
private HttpResponseWritingResultHandler(String outputDir,
Map<String, Object> attributes) {
super(outputDir, "http-response", attributes);
}
@Override
@@ -225,7 +233,6 @@ public abstract class HttpDocumentation {
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());
@@ -238,6 +245,8 @@ public abstract class HttpDocumentation {
}
}
context.putAll(getAttributes());
TemplateEngine templateEngine = (TemplateEngine) result.getRequest()
.getAttribute(TemplateEngine.class.getName());

View File

@@ -17,6 +17,7 @@
package org.springframework.restdocs.hypermedia;
import java.util.Arrays;
import java.util.Map;
import org.springframework.restdocs.RestDocumentationResultHandler;
@@ -48,6 +49,7 @@ public abstract class HypermediaDocumentation {
* snippet for a response's links.
*
* @param outputDir The directory to which the snippet should be written
* @param attributes Attributes made available during rendering of the links snippet
* @param linkExtractor Used to extract the links from the response
* @param descriptors The descriptions of the response's links
* @return the handler
@@ -55,8 +57,9 @@ public abstract class HypermediaDocumentation {
* @see RestDocumentationResultHandler#withLinks(LinkExtractor, LinkDescriptor...)
*/
public static LinkSnippetResultHandler documentLinks(String outputDir,
LinkExtractor linkExtractor, LinkDescriptor... descriptors) {
return new LinkSnippetResultHandler(outputDir, linkExtractor,
Map<String, Object> attributes, LinkExtractor linkExtractor,
LinkDescriptor... descriptors) {
return new LinkSnippetResultHandler(outputDir, attributes, linkExtractor,
Arrays.asList(descriptors));
}
}

View File

@@ -43,13 +43,13 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler {
private final Map<String, LinkDescriptor> descriptorsByRel = new LinkedHashMap<>();
private final Set<String> requiredRels = new HashSet<String>();
private final Set<String> requiredRels = new HashSet<>();
private final LinkExtractor extractor;
LinkSnippetResultHandler(String outputDir, LinkExtractor linkExtractor,
List<LinkDescriptor> descriptors) {
super(outputDir, "links");
LinkSnippetResultHandler(String outputDir, Map<String, Object> attributes,
LinkExtractor linkExtractor, List<LinkDescriptor> descriptors) {
super(outputDir, "links", attributes);
this.extractor = linkExtractor;
for (LinkDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getRel());
@@ -117,6 +117,7 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler {
.getAttribute(TemplateEngine.class.getName());
Map<String, Object> context = new HashMap<>();
context.put("links", createLinksModel());
context.putAll(getAttributes());
writer.print(templateEngine.compileTemplate("links").render(context));
}

View File

@@ -55,8 +55,8 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
private List<FieldDescriptor> fieldDescriptors;
FieldSnippetResultHandler(String outputDir, String type,
List<FieldDescriptor> descriptors) {
super(outputDir, type + "-fields");
Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
super(outputDir, type + "-fields", attributes);
this.templateName = type + "-fields";
for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath());
@@ -83,6 +83,7 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand
}
fields.add(descriptor.toModel());
}
context.putAll(getAttributes());
TemplateEngine templateEngine = (TemplateEngine) result.getRequest()
.getAttribute(TemplateEngine.class.getName());
writer.print(templateEngine.compileTemplate(this.templateName).render(context));

View File

@@ -17,6 +17,7 @@
package org.springframework.restdocs.payload;
import java.util.Arrays;
import java.util.Map;
import org.springframework.restdocs.RestDocumentationResultHandler;
@@ -107,14 +108,16 @@ public abstract class PayloadDocumentation {
* documented.
*
* @param outputDir The directory to which the snippet should be written
* @param attributes Attributes made available during rendering of the links snippet
* @param descriptors The descriptions of the request's fields
* @return the handler
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
* @see #fieldWithPath(String)
*/
public static FieldSnippetResultHandler documentRequestFields(String outputDir,
FieldDescriptor... descriptors) {
return new RequestFieldSnippetResultHandler(outputDir, Arrays.asList(descriptors));
Map<String, Object> attributes, FieldDescriptor... descriptors) {
return new RequestFieldSnippetResultHandler(outputDir, attributes,
Arrays.asList(descriptors));
}
/**
@@ -129,13 +132,14 @@ public abstract class PayloadDocumentation {
* documented.
*
* @param outputDir The directory to which the snippet should be written
* @param attributes Attributes made available during rendering of the links snippet
* @param descriptors The descriptions of the response's fields
* @return the handler
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
*/
public static FieldSnippetResultHandler documentResponseFields(String outputDir,
FieldDescriptor... descriptors) {
return new ResponseFieldSnippetResultHandler(outputDir,
Map<String, Object> attributes, FieldDescriptor... descriptors) {
return new ResponseFieldSnippetResultHandler(outputDir, attributes,
Arrays.asList(descriptors));
}

View File

@@ -18,6 +18,7 @@ package org.springframework.restdocs.payload;
import java.io.IOException;
import java.io.Reader;
import java.util.List;
import java.util.Map;
import org.springframework.test.web.servlet.MvcResult;
@@ -28,8 +29,9 @@ import org.springframework.test.web.servlet.MvcResult;
*/
public class RequestFieldSnippetResultHandler extends FieldSnippetResultHandler {
RequestFieldSnippetResultHandler(String outputDir, List<FieldDescriptor> descriptors) {
super(outputDir, "request", descriptors);
RequestFieldSnippetResultHandler(String outputDir, Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
super(outputDir, "request", attributes, descriptors);
}
@Override

View File

@@ -19,6 +19,7 @@ import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
import org.springframework.test.web.servlet.MvcResult;
@@ -29,8 +30,9 @@ import org.springframework.test.web.servlet.MvcResult;
*/
public class ResponseFieldSnippetResultHandler extends FieldSnippetResultHandler {
ResponseFieldSnippetResultHandler(String outputDir, List<FieldDescriptor> descriptors) {
super(outputDir, "response", descriptors);
ResponseFieldSnippetResultHandler(String outputDir, Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
super(outputDir, "response", attributes, descriptors);
}
@Override

View File

@@ -44,8 +44,8 @@ public class QueryParametersSnippetResultHandler extends SnippetWritingResultHan
private final Map<String, ParameterDescriptor> descriptorsByName = new LinkedHashMap<>();
protected QueryParametersSnippetResultHandler(String outputDir,
ParameterDescriptor... descriptors) {
super(outputDir, "query-parameters");
Map<String, Object> attributes, ParameterDescriptor... descriptors) {
super(outputDir, "query-parameters", attributes);
for (ParameterDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getName());
Assert.hasText(descriptor.getDescription());
@@ -98,6 +98,7 @@ public class QueryParametersSnippetResultHandler extends SnippetWritingResultHan
parameters.add(entry.getValue().toModel());
}
context.put("parameters", parameters);
context.putAll(getAttributes());
writer.print(templateEngine.compileTemplate("query-parameters").render(context));
}

View File

@@ -16,6 +16,8 @@
package org.springframework.restdocs.request;
import java.util.Map;
import org.springframework.restdocs.RestDocumentationResultHandler;
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
@@ -35,13 +37,15 @@ public abstract class RequestDocumentation {
* documenting a request's query parameters
*
* @param outputDir The directory to which the snippet should be written
* @param attributes Attributes made available during rendering of the query
* parameters snippet
* @param descriptors The descriptions of the parameters in the request's query string
* @return the result handler
* @see RestDocumentationResultHandler#withQueryParameters(ParameterDescriptor...)
*/
public static SnippetWritingResultHandler documentQueryParameters(String outputDir,
ParameterDescriptor... descriptors) {
return new QueryParametersSnippetResultHandler(outputDir, descriptors);
Map<String, Object> attributes, ParameterDescriptor... descriptors) {
return new QueryParametersSnippetResultHandler(outputDir, attributes, descriptors);
}
/**

View File

@@ -23,6 +23,8 @@ import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
import org.springframework.restdocs.config.RestDocumentationContext;
import org.springframework.test.web.servlet.MvcResult;
@@ -35,13 +37,19 @@ import org.springframework.test.web.servlet.ResultHandler;
*/
public abstract class SnippetWritingResultHandler implements ResultHandler {
private String outputDir;
private final Map<String, Object> attributes = new HashMap<>();
private String fileName;
private final String outputDir;
protected SnippetWritingResultHandler(String outputDir, String fileName) {
private final String fileName;
protected SnippetWritingResultHandler(String outputDir, String fileName,
Map<String, Object> attributes) {
this.outputDir = outputDir;
this.fileName = fileName;
if (attributes != null) {
this.attributes.putAll(attributes);
}
}
protected abstract void handle(MvcResult result, PrintWriter writer)
@@ -54,6 +62,10 @@ public abstract class SnippetWritingResultHandler implements ResultHandler {
}
}
protected Map<String, Object> getAttributes() {
return this.attributes;
}
private Writer createWriter() throws IOException {
File outputFile = new OutputFileResolver().resolve(this.outputDir, this.fileName
+ ".adoc");

View File

@@ -16,6 +16,11 @@
package org.springframework.restdocs.curl;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.Attributes.attributes;
import static org.springframework.restdocs.Attributes.key;
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequest;
import static org.springframework.restdocs.test.SnippetMatchers.codeBlock;
import static org.springframework.restdocs.test.StubMvcResult.result;
@@ -29,9 +34,13 @@ import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
@@ -54,14 +63,14 @@ public class CurlDocumentationTests {
public void getRequest() throws IOException {
this.snippet.expectCurlRequest("get-request").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo' -i"));
documentCurlRequest("get-request").handle(result(get("/foo")));
documentCurlRequest("get-request", null).handle(result(get("/foo")));
}
@Test
public void nonGetRequest() throws IOException {
this.snippet.expectCurlRequest("non-get-request").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo' -i -X POST"));
documentCurlRequest("non-get-request").handle(result(post("/foo")));
documentCurlRequest("non-get-request", null).handle(result(post("/foo")));
}
@Test
@@ -69,7 +78,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("request-with-content").withContents(
codeBlock("bash")
.content("$ curl 'http://localhost/foo' -i -d 'content'"));
documentCurlRequest("request-with-content").handle(
documentCurlRequest("request-with-content", null).handle(
result(get("/foo").content("content")));
}
@@ -79,7 +88,7 @@ public class CurlDocumentationTests {
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo?param=value' -i"));
documentCurlRequest("request-with-query-string").handle(
documentCurlRequest("request-with-query-string", null).handle(
result(get("/foo?param=value")));
}
@@ -87,7 +96,7 @@ public class CurlDocumentationTests {
public void requestWithOneParameter() throws IOException {
this.snippet.expectCurlRequest("request-with-one-parameter").withContents(
codeBlock("bash").content("$ curl 'http://localhost/foo?k1=v1' -i"));
documentCurlRequest("request-with-one-parameter").handle(
documentCurlRequest("request-with-one-parameter", null).handle(
result(get("/foo").param("k1", "v1")));
}
@@ -96,7 +105,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("request-with-multiple-parameters").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo?k1=v1&k1=v1-bis&k2=v2' -i"));
documentCurlRequest("request-with-multiple-parameters").handle(
documentCurlRequest("request-with-multiple-parameters", null).handle(
result(get("/foo").param("k1", "v1").param("k2", "v2")
.param("k1", "v1-bis")));
}
@@ -107,7 +116,7 @@ public class CurlDocumentationTests {
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo?k1=foo+bar%26' -i"));
documentCurlRequest("request-with-url-encoded-parameter").handle(
documentCurlRequest("request-with-url-encoded-parameter", null).handle(
result(get("/foo").param("k1", "foo bar&")));
}
@@ -116,7 +125,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST -d 'k1=v1'"));
documentCurlRequest("post-request-with-one-parameter").handle(
documentCurlRequest("post-request-with-one-parameter", null).handle(
result(post("/foo").param("k1", "v1")));
}
@@ -127,7 +136,7 @@ public class CurlDocumentationTests {
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST"
+ " -d 'k1=v1&k1=v1-bis&k2=v2'"));
documentCurlRequest("post-request-with-multiple-parameters").handle(
documentCurlRequest("post-request-with-multiple-parameters", null).handle(
result(post("/foo").param("k1", "v1", "v1-bis").param("k2", "v2")));
}
@@ -138,7 +147,7 @@ public class CurlDocumentationTests {
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X POST -d 'k1=a%26b'"));
documentCurlRequest("post-request-with-url-encoded-parameter").handle(
documentCurlRequest("post-request-with-url-encoded-parameter", null).handle(
result(post("/foo").param("k1", "a&b")));
}
@@ -147,7 +156,7 @@ public class CurlDocumentationTests {
this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT -d 'k1=v1'"));
documentCurlRequest("put-request-with-one-parameter").handle(
documentCurlRequest("put-request-with-one-parameter", null).handle(
result(put("/foo").param("k1", "v1")));
}
@@ -158,7 +167,7 @@ public class CurlDocumentationTests {
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT"
+ " -d 'k1=v1&k1=v1-bis&k2=v2'"));
documentCurlRequest("put-request-with-multiple-parameters").handle(
documentCurlRequest("put-request-with-multiple-parameters", null).handle(
result(put("/foo").param("k1", "v1", "v1-bis").param("k2", "v2")));
}
@@ -168,7 +177,7 @@ public class CurlDocumentationTests {
.withContents(
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i -X PUT -d 'k1=a%26b'"));
documentCurlRequest("put-request-with-url-encoded-parameter").handle(
documentCurlRequest("put-request-with-url-encoded-parameter", null).handle(
result(put("/foo").param("k1", "a&b")));
}
@@ -178,7 +187,7 @@ public class CurlDocumentationTests {
codeBlock("bash").content(
"$ curl 'http://localhost/foo' -i"
+ " -H 'Content-Type: application/json' -H 'a: alpha'"));
documentCurlRequest("request-with-headers").handle(
documentCurlRequest("request-with-headers", null).handle(
result(get("/foo").contentType(MediaType.APPLICATION_JSON).header("a",
"alpha")));
}
@@ -189,7 +198,7 @@ public class CurlDocumentationTests {
codeBlock("bash").content("$ curl 'http://localhost:8080/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(8080);
documentCurlRequest("http-with-non-standard-port").handle(result(request));
documentCurlRequest("http-with-non-standard-port", null).handle(result(request));
}
@Test
@@ -199,7 +208,7 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(443);
request.setScheme("https");
documentCurlRequest("https-with-standard-port").handle(result(request));
documentCurlRequest("https-with-standard-port", null).handle(result(request));
}
@Test
@@ -209,7 +218,7 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerPort(8443);
request.setScheme("https");
documentCurlRequest("https-with-non-standard-port").handle(result(request));
documentCurlRequest("https-with-non-standard-port", null).handle(result(request));
}
@Test
@@ -218,7 +227,7 @@ public class CurlDocumentationTests {
codeBlock("bash").content("$ curl 'http://api.example.com/foo' -i"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
documentCurlRequest("request-with-custom-host").handle(result(request));
documentCurlRequest("request-with-custom-host", null).handle(result(request));
}
@Test
@@ -230,7 +239,7 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
request.setContextPath("/v3");
documentCurlRequest("request-with-custom-context-with-slash").handle(
documentCurlRequest("request-with-custom-context-with-slash", null).handle(
result(request));
}
@@ -243,7 +252,7 @@ public class CurlDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
request.setContextPath("v3");
documentCurlRequest("request-with-custom-context-without-slash").handle(
documentCurlRequest("request-with-custom-context-without-slash", null).handle(
result(request));
}
@@ -256,7 +265,7 @@ public class CurlDocumentationTests {
.withContents(codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("metadata",
"{\"description\": \"foo\"}".getBytes());
documentCurlRequest("multipart-post-no-original-filename").handle(
documentCurlRequest("multipart-post-no-original-filename", null).handle(
result(fileUpload("/upload").file(multipartFile)));
}
@@ -270,7 +279,7 @@ public class CurlDocumentationTests {
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", MediaType.IMAGE_PNG_VALUE,
"bytes".getBytes());
documentCurlRequest("multipart-post-with-content-type").handle(
documentCurlRequest("multipart-post-with-content-type", null).handle(
result(fileUpload("/upload").file(multipartFile)));
}
@@ -283,7 +292,7 @@ public class CurlDocumentationTests {
codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "bytes".getBytes());
documentCurlRequest("multipart-post").handle(
documentCurlRequest("multipart-post", null).handle(
result(fileUpload("/upload").file(multipartFile)));
}
@@ -297,9 +306,26 @@ public class CurlDocumentationTests {
codeBlock("bash").content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "bytes".getBytes());
documentCurlRequest("multipart-post").handle(
documentCurlRequest("multipart-post", null).handle(
result(fileUpload("/upload").file(multipartFile)
.param("a", "apple", "avocado").param("b", "banana")));
}
@Test
public void customAttributes() throws IOException {
this.snippet.expectCurlRequest("custom-attributes").withContents(
containsString("curl request title"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("curl-request"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/curl-request-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentCurlRequest("custom-attributes",
attributes(key("title").value("curl request title"))).handle(
result(request));
}
}

View File

@@ -16,8 +16,13 @@
package org.springframework.restdocs.http;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.OK;
import static org.springframework.restdocs.Attributes.attributes;
import static org.springframework.restdocs.Attributes.key;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpRequest;
import static org.springframework.restdocs.http.HttpDocumentation.documentHttpResponse;
import static org.springframework.restdocs.test.SnippetMatchers.httpRequest;
@@ -35,11 +40,15 @@ import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import org.springframework.restdocs.test.ExpectedSnippet;
/**
@@ -61,7 +70,7 @@ public class HttpDocumentationTests {
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "localhost").header(
"Alpha", "a"));
documentHttpRequest("get-request").handle(
documentHttpRequest("get-request", null).handle(
result(get("/foo").header("Alpha", "a")));
}
@@ -70,7 +79,7 @@ public class HttpDocumentationTests {
this.snippet.expectHttpRequest("get-request-with-query-string").withContents(
httpRequest(GET, "/foo?bar=baz").header(HttpHeaders.HOST, "localhost"));
documentHttpRequest("get-request-with-query-string").handle(
documentHttpRequest("get-request-with-query-string", null).handle(
result(get("/foo?bar=baz")));
}
@@ -79,7 +88,7 @@ public class HttpDocumentationTests {
this.snippet.expectHttpRequest("get-request-with-parameter").withContents(
httpRequest(GET, "/foo?b%26r=baz").header(HttpHeaders.HOST, "localhost"));
documentHttpRequest("get-request-with-parameter").handle(
documentHttpRequest("get-request-with-parameter", null).handle(
result(get("/foo").param("b&r", "baz")));
}
@@ -89,7 +98,7 @@ public class HttpDocumentationTests {
httpRequest(POST, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world"));
documentHttpRequest("post-request-with-content").handle(
documentHttpRequest("post-request-with-content", null).handle(
result(post("/foo").content("Hello, world")));
}
@@ -100,7 +109,7 @@ public class HttpDocumentationTests {
.header("Content-Type", "application/x-www-form-urlencoded")
.content("b%26r=baz&a=alpha"));
documentHttpRequest("post-request-with-parameter").handle(
documentHttpRequest("post-request-with-parameter", null).handle(
result(post("/foo").param("b&r", "baz").param("a", "alpha")));
}
@@ -110,7 +119,7 @@ public class HttpDocumentationTests {
httpRequest(PUT, "/foo").header(HttpHeaders.HOST, "localhost").content(
"Hello, world"));
documentHttpRequest("put-request-with-content").handle(
documentHttpRequest("put-request-with-content", null).handle(
result(put("/foo").content("Hello, world")));
}
@@ -121,14 +130,14 @@ public class HttpDocumentationTests {
.header("Content-Type", "application/x-www-form-urlencoded")
.content("b%26r=baz&a=alpha"));
documentHttpRequest("put-request-with-parameter").handle(
documentHttpRequest("put-request-with-parameter", null).handle(
result(put("/foo").param("b&r", "baz").param("a", "alpha")));
}
@Test
public void basicResponse() throws IOException {
this.snippet.expectHttpResponse("basic-response").withContents(httpResponse(OK));
documentHttpResponse("basic-response").handle(result());
documentHttpResponse("basic-response", null).handle(result());
}
@Test
@@ -138,7 +147,7 @@ public class HttpDocumentationTests {
MockHttpServletResponse response = new MockHttpServletResponse();
response.setStatus(BAD_REQUEST.value());
documentHttpResponse("non-ok-response").handle(result(response));
documentHttpResponse("non-ok-response", null).handle(result(response));
}
@Test
@@ -151,7 +160,7 @@ public class HttpDocumentationTests {
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setHeader("a", "alpha");
documentHttpResponse("response-with-headers").handle(result(response));
documentHttpResponse("response-with-headers", null).handle(result(response));
}
@Test
@@ -160,7 +169,7 @@ public class HttpDocumentationTests {
httpResponse(OK).content("content"));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter().append("content");
documentHttpResponse("response-with-content").handle(result(response));
documentHttpResponse("response-with-content", null).handle(result(response));
}
@Test
@@ -175,7 +184,7 @@ public class HttpDocumentationTests {
.content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "<< data >>".getBytes());
documentHttpRequest("multipart-post").handle(
documentHttpRequest("multipart-post", null).handle(
result(fileUpload("/upload").file(multipartFile)));
}
@@ -198,7 +207,7 @@ public class HttpDocumentationTests {
.content(expectedContent));
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", null, "<< data >>".getBytes());
documentHttpRequest("multipart-post").handle(
documentHttpRequest("multipart-post", null).handle(
result(fileUpload("/upload").file(multipartFile)
.param("a", "apple", "avocado").param("b", "banana")));
}
@@ -217,7 +226,7 @@ public class HttpDocumentationTests {
MockMultipartFile multipartFile = new MockMultipartFile("image",
"documents/images/example.png", MediaType.IMAGE_PNG_VALUE,
"<< data >>".getBytes());
documentHttpRequest("multipart-post-with-content-type").handle(
documentHttpRequest("multipart-post-with-content-type", null).handle(
result(fileUpload("/upload").file(multipartFile)));
}
@@ -229,7 +238,8 @@ public class HttpDocumentationTests {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
request.setServerName("api.example.com");
documentHttpRequest("get-request-custom-server-name").handle(result(request));
documentHttpRequest("get-request-custom-server-name", null).handle(
result(request));
}
@Test
@@ -237,10 +247,44 @@ public class HttpDocumentationTests {
this.snippet.expectHttpRequest("get-request-custom-host").withContents(
httpRequest(GET, "/foo").header(HttpHeaders.HOST, "api.example.com"));
documentHttpRequest("get-request-custom-host").handle(
documentHttpRequest("get-request-custom-host", null).handle(
result(get("/foo").header(HttpHeaders.HOST, "api.example.com")));
}
@Test
public void requestWithCustomSnippetAttributes() throws IOException {
this.snippet.expectHttpRequest("request-with-snippet-attributes").withContents(
containsString("Title for the request"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("http-request"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/http-request-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentHttpRequest("request-with-snippet-attributes",
attributes(key("title").value("Title for the request"))).handle(
result(request));
}
@Test
public void responseWithCustomSnippetAttributes() throws IOException {
this.snippet.expectHttpResponse("response-with-snippet-attributes").withContents(
containsString("Title for the response"));
MockHttpServletRequest request = new MockHttpServletRequest();
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("http-response"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/http-response-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentHttpResponse("response-with-snippet-attributes",
attributes(key("title").value("Title for the response"))).handle(
result(request));
}
private String createPart(String content) {
return this.createPart(content, true);
}

View File

@@ -17,8 +17,11 @@
package org.springframework.restdocs.hypermedia;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.Attributes.attributes;
import static org.springframework.restdocs.Attributes.key;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
import static org.springframework.restdocs.test.StubMvcResult.result;
@@ -57,7 +60,7 @@ public class HypermediaDocumentationTests {
this.thrown.expect(SnippetGenerationException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " documented: [foo]"));
documentLinks("undocumented-link",
documentLinks("undocumented-link", null,
new StubLinkExtractor().withLinks(new Link("foo", "bar"))).handle(
result());
}
@@ -67,7 +70,7 @@ public class HypermediaDocumentationTests {
this.thrown.expect(SnippetGenerationException.class);
this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " found in the response: [foo]"));
documentLinks("missing-link", new StubLinkExtractor(),
documentLinks("missing-link", null, new StubLinkExtractor(),
new LinkDescriptor("foo").description("bar")).handle(result());
}
@@ -76,7 +79,7 @@ public class HypermediaDocumentationTests {
this.snippet.expectLinks("documented-optional-link").withContents( //
tableWithHeader("Relation", "Description") //
.row("foo", "bar"));
documentLinks("documented-optional-link",
documentLinks("documented-optional-link", null,
new StubLinkExtractor().withLinks(new Link("foo", "blah")),
new LinkDescriptor("foo").description("bar").optional()).handle(result());
}
@@ -86,7 +89,7 @@ public class HypermediaDocumentationTests {
this.snippet.expectLinks("missing-optional-link").withContents( //
tableWithHeader("Relation", "Description") //
.row("foo", "bar"));
documentLinks("missing-optional-link", new StubLinkExtractor(),
documentLinks("missing-optional-link", null, new StubLinkExtractor(),
new LinkDescriptor("foo").description("bar").optional()).handle(result());
}
@@ -96,7 +99,7 @@ public class HypermediaDocumentationTests {
this.thrown.expectMessage(equalTo("Links with the following relations were not"
+ " documented: [a]. Links with the following relations were not"
+ " found in the response: [foo]"));
documentLinks("undocumented-link-and-missing-link",
documentLinks("undocumented-link-and-missing-link", null,
new StubLinkExtractor().withLinks(new Link("a", "alpha")),
new LinkDescriptor("foo").description("bar")).handle(result());
}
@@ -109,14 +112,15 @@ public class HypermediaDocumentationTests {
.row("b", "two"));
documentLinks(
"documented-links",
null,
new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b",
"bravo")), new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two")).handle(result());
}
@Test
public void linksWithCustomAttributes() throws IOException {
this.snippet.expectLinks("documented-links").withContents( //
public void linksWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectLinks("links-with-custom-descriptor-attributes").withContents( //
tableWithHeader("Relation", "Description", "Foo") //
.row("a", "one", "alpha") //
.row("b", "two", "bravo"));
@@ -129,12 +133,34 @@ public class HypermediaDocumentationTests {
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentLinks(
"documented-links",
"links-with-custom-descriptor-attributes",
null,
new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b",
"bravo")),
new LinkDescriptor("a").description("one").attribute("foo", "alpha"),
new LinkDescriptor("b").description("two").attribute("foo", "bravo"))
.handle(result(request));
new LinkDescriptor("a").description("one").attributes(
key("foo").value("alpha")),
new LinkDescriptor("b").description("two").attributes(
key("foo").value("bravo"))).handle(result(request));
}
@Test
public void linksWithCustomAttribute() throws IOException {
this.snippet.expectLinks("links-with-custom-attribute").withContents(
startsWith(".Title for the links"));
MockHttpServletRequest request = new MockHttpServletRequest();
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("links"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/links-with-title.snippet"));
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
documentLinks(
"links-with-custom-attribute",
attributes(key("title").value("Title for the links")),
new StubLinkExtractor().withLinks(new Link("a", "alpha"), new Link("b",
"bravo")), new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two")).handle(result(request));
}
private static class StubLinkExtractor implements LinkExtractor {

View File

@@ -20,6 +20,8 @@ import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.Attributes.attributes;
import static org.springframework.restdocs.Attributes.key;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
@@ -62,7 +64,7 @@ public class PayloadDocumentationTests {
.row("a.c", "String", "two") //
.row("a", "Object", "three"));
documentRequestFields("map-request-with-fields",
documentRequestFields("map-request-with-fields", null,
fieldWithPath("a.b").description("one"),
fieldWithPath("a.c").description("two"),
fieldWithPath("a").description("three")).handle(
@@ -77,7 +79,7 @@ public class PayloadDocumentationTests {
.row("[]a.c", "String", "two") //
.row("[]a", "Object", "three"));
documentRequestFields("array-request-with-fields",
documentRequestFields("array-request-with-fields", null,
fieldWithPath("[]a.b").description("one"),
fieldWithPath("[]a.c").description("two"),
fieldWithPath("[]a").description("three")).handle(
@@ -100,7 +102,7 @@ public class PayloadDocumentationTests {
response.getWriter().append(
"{\"id\": 67,\"date\": \"2015-01-20\",\"assets\":"
+ " [{\"id\":356,\"name\": \"sample\"}]}");
documentResponseFields("map-response-with-fields",
documentResponseFields("map-response-with-fields", null,
fieldWithPath("id").description("one"),
fieldWithPath("date").description("two"),
fieldWithPath("assets").description("three"),
@@ -121,7 +123,7 @@ public class PayloadDocumentationTests {
MockHttpServletResponse response = new MockHttpServletResponse();
response.getWriter()
.append("[{\"a\": {\"b\": 5}},{\"a\": {\"c\": \"charlie\"}}]");
documentResponseFields("array-response-with-fields",
documentResponseFields("array-response-with-fields", null,
fieldWithPath("[]a.b").description("one"),
fieldWithPath("[]a.c").description("two"),
fieldWithPath("[]a").description("three")).handle(result(response));
@@ -145,7 +147,7 @@ public class PayloadDocumentationTests {
this.thrown
.expectMessage(startsWith("The following parts of the payload were not"
+ " documented:"));
documentRequestFields("undocumented-request-fields").handle(
documentRequestFields("undocumented-request-fields", null).handle(
result(get("/foo").content("{\"a\": 5}")));
}
@@ -155,7 +157,7 @@ public class PayloadDocumentationTests {
this.thrown
.expectMessage(equalTo("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
documentRequestFields("missing-request-fields",
documentRequestFields("missing-request-fields", null,
fieldWithPath("a.b").description("one")).handle(
result(get("/foo").content("{}")));
}
@@ -163,7 +165,7 @@ public class PayloadDocumentationTests {
@Test
public void missingOptionalRequestFieldWithNoTypeProvided() throws IOException {
this.thrown.expect(FieldTypeRequiredException.class);
documentRequestFields("missing-optional-request-field-with-no-type",
documentRequestFields("missing-optional-request-field-with-no-type", null,
fieldWithPath("a.b").description("one").optional()).handle(
result(get("/foo").content("{ }")));
}
@@ -178,20 +180,20 @@ public class PayloadDocumentationTests {
.expectMessage(endsWith("Fields with the following paths were not found"
+ " in the payload: [a.b]"));
documentRequestFields("undocumented-request-field-and-missing-request-field",
fieldWithPath("a.b").description("one")).handle(
null, fieldWithPath("a.b").description("one")).handle(
result(get("/foo").content("{ \"a\": { \"c\": 5 }}")));
}
@Test
public void requestFieldsWithCustomAttributes() throws IOException {
this.snippet.expectRequestFields("request-fields-with-custom-attributes")
.withContents( //
tableWithHeader("Path", "Type", "Description", "Foo") //
.row("a.b", "Number", "one", "alpha") //
.row("a.c", "String", "two", "bravo") //
.row("a", "Object", "three", "charlie"));
public void requestFieldsWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectRequestFields(
"request-fields-with-custom-descriptor-attributes").withContents( //
tableWithHeader("Path", "Type", "Description", "Foo") //
.row("a.b", "Number", "one", "alpha") //
.row("a.c", "String", "two", "bravo") //
.row("a", "Object", "three", "charlie"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("fields"))
when(resolver.resolveTemplateResource("request-fields"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet"));
@@ -199,15 +201,19 @@ public class PayloadDocumentationTests {
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.setContent("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes());
documentRequestFields("request-fields-with-custom-attributes",
fieldWithPath("a.b").description("one").attribute("foo", "alpha"),
fieldWithPath("a.c").description("two").attribute("foo", "bravo"),
fieldWithPath("a").description("three").attribute("foo", "charlie"))
.handle(result(request));
documentRequestFields(
"request-fields-with-custom-descriptor-attributes",
null,
fieldWithPath("a.b").description("one").attributes(
key("foo").value("alpha")),
fieldWithPath("a.c").description("two").attributes(
key("foo").value("bravo")),
fieldWithPath("a").description("three").attributes(
key("foo").value("charlie"))).handle(result(request));
}
@Test
public void responseFieldsWithCustomAttributes() throws IOException {
public void responseFieldsWithCustomDescriptorAttributes() throws IOException {
this.snippet.expectResponseFields("response-fields-with-custom-attributes")
.withContents( //
tableWithHeader("Path", "Type", "Description", "Foo") //
@@ -215,7 +221,7 @@ public class PayloadDocumentationTests {
.row("a.c", "String", "two", "bravo") //
.row("a", "Object", "three", "charlie"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("fields"))
when(resolver.resolveTemplateResource("response-fields"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet"));
@@ -224,10 +230,53 @@ public class PayloadDocumentationTests {
resolver));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getOutputStream().print("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}");
documentResponseFields("response-fields-with-custom-attributes",
fieldWithPath("a.b").description("one").attribute("foo", "alpha"),
fieldWithPath("a.c").description("two").attribute("foo", "bravo"),
fieldWithPath("a").description("three").attribute("foo", "charlie"))
.handle(result(request, response));
documentResponseFields(
"response-fields-with-custom-attributes",
null,
fieldWithPath("a.b").description("one").attributes(
key("foo").value("alpha")),
fieldWithPath("a.c").description("two").attributes(
key("foo").value("bravo")),
fieldWithPath("a").description("three").attributes(
key("foo").value("charlie"))).handle(result(request, response));
}
@Test
public void requestFieldsWithCustomAttributes() throws IOException {
this.snippet.expectRequestFields("request-fields-with-custom-attributes")
.withContents(startsWith(".Custom title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-fields"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/request-fields-with-title.snippet"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.setContent("{\"a\": \"foo\"}".getBytes());
MockHttpServletResponse response = new MockHttpServletResponse();
documentRequestFields("request-fields-with-custom-attributes",
attributes(key("title").value("Custom title")),
fieldWithPath("a").description("one")).handle(result(request, response));
}
@Test
public void responseFieldsWithCustomAttributes() throws IOException {
this.snippet.expectResponseFields("response-fields-with-custom-attributes")
.withContents(startsWith(".Custom title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("response-fields"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/response-fields-with-title.snippet"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
MockHttpServletResponse response = new MockHttpServletResponse();
response.getOutputStream().print("{\"a\": \"foo\"}");
documentResponseFields("response-fields-with-custom-attributes",
attributes(key("title").value("Custom title")),
fieldWithPath("a").description("one")).handle(result(request, response));
}
}

View File

@@ -17,8 +17,11 @@
package org.springframework.restdocs.request;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.Attributes.attributes;
import static org.springframework.restdocs.Attributes.key;
import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader;
@@ -57,7 +60,7 @@ public class RequestDocumentationTests {
this.thrown
.expectMessage(equalTo("Query parameters with the following names were"
+ " not documented: [a]"));
documentQueryParameters("undocumented-parameter").handle(
documentQueryParameters("undocumented-parameter", null).handle(
result(get("/").param("a", "alpha")));
}
@@ -67,7 +70,7 @@ public class RequestDocumentationTests {
this.thrown
.expectMessage(equalTo("Query parameters with the following names were"
+ " not found in the request: [a]"));
documentQueryParameters("missing-parameter",
documentQueryParameters("missing-parameter", null,
parameterWithName("a").description("one")).handle(result(get("/")));
}
@@ -78,7 +81,7 @@ public class RequestDocumentationTests {
.expectMessage(equalTo("Query parameters with the following names were"
+ " not documented: [b]. Query parameters with the following"
+ " names were not found in the request: [a]"));
documentQueryParameters("undocumented-parameter-missing-parameter",
documentQueryParameters("undocumented-parameter-missing-parameter", null,
parameterWithName("a").description("one")).handle(
result(get("/").param("b", "bravo")));
}
@@ -89,7 +92,7 @@ public class RequestDocumentationTests {
.withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row(
"b", "two"));
documentQueryParameters("parameter-snippet-request-parameters",
documentQueryParameters("parameter-snippet-request-parameters", null,
parameterWithName("a").description("one"),
parameterWithName("b").description("two")).handle(
result(get("/").param("a", "bravo").param("b", "bravo")));
@@ -101,15 +104,16 @@ public class RequestDocumentationTests {
.withContents(
tableWithHeader("Parameter", "Description").row("a", "one").row(
"b", "two"));
documentQueryParameters("parameter-snippet-request-uri-query-string",
documentQueryParameters("parameter-snippet-request-uri-query-string", null,
parameterWithName("a").description("one"),
parameterWithName("b").description("two")).handle(
result(get("/?a=alpha&b=bravo")));
}
@Test
public void parametersWithCustomAttributes() throws IOException {
this.snippet.expectQueryParameters("parameters-with-custom-attributes")
public void parametersWithCustomDescriptorAttributes() throws IOException {
this.snippet
.expectQueryParameters("parameters-with-custom-descriptor-attributes")
.withContents(
tableWithHeader("Parameter", "Description", "Foo").row("a",
"one", "alpha").row("b", "two", "bravo"));
@@ -123,10 +127,36 @@ public class RequestDocumentationTests {
resolver));
request.addParameter("a", "bravo");
request.addParameter("b", "bravo");
documentQueryParameters("parameters-with-custom-attributes",
parameterWithName("a").description("one").attribute("foo", "alpha"),
parameterWithName("b").description("two").attribute("foo", "bravo"))
.handle(result(request));
documentQueryParameters(
"parameters-with-custom-descriptor-attributes",
null,
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")),
parameterWithName("b").description("two").attributes(
key("foo").value("bravo"))).handle(result(request));
}
@Test
public void parametersWithCustomAttributes() throws IOException {
this.snippet.expectQueryParameters("parameters-with-custom-attributes")
.withContents(startsWith(".The title"));
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("query-parameters"))
.thenReturn(
new FileSystemResource(
"src/test/resources/custom-snippet-templates/query-parameters-with-title.snippet"));
MockHttpServletRequest request = new MockHttpServletRequest();
request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine(
resolver));
request.addParameter("a", "bravo");
request.addParameter("b", "bravo");
documentQueryParameters(
"parameters-with-custom-attributes",
attributes(key("title").value("The title")),
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")),
parameterWithName("b").description("two").attributes(
key("foo").value("bravo"))).handle(result(request));
}
}

View File

@@ -0,0 +1,5 @@
[source,bash]
.{{title}}
----
$ curl {{arguments}}
----

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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