Make the built-in snippets more extensible

The commit opens up all of the default snippets so that it's easier
to extend them and modify their behaviour. All of the built-in
snippets are now public with protected constructors. Also, where
appropriate the models used by the snippets have been made more
fine-grained.

Closes gh-73
This commit is contained in:
Andy Wilkinson
2015-09-08 10:30:50 +01:00
parent de095eb9fe
commit c2da4c912a
21 changed files with 249 additions and 84 deletions

View File

@@ -36,31 +36,43 @@ import org.springframework.util.StringUtils;
* A {@link Snippet} that documents the curl command for a request.
*
* @author Andy Wilkinson
* @see CurlDocumentation#curlRequest()
* @see CurlDocumentation#curlRequest(Map)
*/
class CurlRequestSnippet extends TemplatedSnippet {
public class CurlRequestSnippet extends TemplatedSnippet {
CurlRequestSnippet() {
/**
* Creates a new {@code CurlRequestSnippet} with no additional attributes.
*/
protected CurlRequestSnippet() {
this(null);
}
CurlRequestSnippet(Map<String, Object> attributes) {
/**
* Creates a new {@code CurlRequestSnippet} with the given additional
* {@code attributes} that will be included in the model during template rendering.
*
* @param attributes The additional attributes
*/
protected CurlRequestSnippet(Map<String, Object> attributes) {
super("curl-request", attributes);
}
@Override
public Map<String, Object> createModel(Operation operation) throws IOException {
protected Map<String, Object> createModel(Operation operation) throws IOException {
Map<String, Object> model = new HashMap<String, Object>();
model.put("arguments", getCurlCommandArguments(operation));
model.put("url", getUrl(operation));
model.put("options", getOptions(operation));
return model;
}
private String getCurlCommandArguments(Operation operation) throws IOException {
private String getUrl(Operation operation) {
return String.format("'%s'", operation.getRequest().getUri());
}
private String getOptions(Operation operation) throws IOException {
StringWriter command = new StringWriter();
PrintWriter printer = new PrintWriter(command);
printer.print("'");
printer.print(operation.getRequest().getUri());
printer.print("'");
writeOptionToIncludeHeadersInOutput(printer);
writeHttpMethodIfNecessary(operation.getRequest(), printer);
writeHeaders(operation.getRequest(), printer);
@@ -72,7 +84,7 @@ class CurlRequestSnippet extends TemplatedSnippet {
}
private void writeOptionToIncludeHeadersInOutput(PrintWriter writer) {
writer.print(" -i");
writer.print("-i");
}
private void writeHttpMethodIfNecessary(OperationRequest request, PrintWriter writer) {

View File

@@ -39,21 +39,32 @@ import org.springframework.util.StringUtils;
* A {@link Snippet} that documents an HTTP request.
*
* @author Andy Wilkinson
* @see HttpDocumentation#httpRequest()
* @see HttpDocumentation#httpRequest(Map)
*/
class HttpRequestSnippet extends TemplatedSnippet {
public class HttpRequestSnippet extends TemplatedSnippet {
private static final String MULTIPART_BOUNDARY = "6o2knFse3p53ty9dmcQvWAIx1zInP11uCfbm";
HttpRequestSnippet() {
/**
* Creates a new {@code HttpRequestSnippet} with no additional attributes.
*/
protected HttpRequestSnippet() {
this(null);
}
HttpRequestSnippet(Map<String, Object> attributes) {
/**
* Creates a new {@code HttpRequestSnippet} with the given additional
* {@code attributes} that will be included in the model during template rendering.
*
* @param attributes The additional attributes
*/
protected HttpRequestSnippet(Map<String, Object> attributes) {
super("http-request", attributes);
}
@Override
public Map<String, Object> createModel(Operation operation) throws IOException {
protected Map<String, Object> createModel(Operation operation) throws IOException {
Map<String, Object> model = new HashMap<String, Object>();
model.put("method", operation.getRequest().getMethod());
model.put(
@@ -61,8 +72,7 @@ class HttpRequestSnippet extends TemplatedSnippet {
operation.getRequest().getUri().getRawPath()
+ (StringUtils.hasText(operation.getRequest().getUri()
.getRawQuery()) ? "?"
+ operation.getRequest().getUri().getRawQuery()
: ""));
+ operation.getRequest().getUri().getRawQuery() : ""));
model.put("headers", getHeaders(operation.getRequest()));
model.put("requestBody", getRequestBody(operation.getRequest()));
return model;

View File

@@ -33,19 +33,30 @@ import org.springframework.restdocs.snippet.TemplatedSnippet;
* A {@link Snippet} that documents an HTTP response.
*
* @author Andy Wilkinson
* @see HttpDocumentation#httpResponse()
* @see HttpDocumentation#httpResponse(Map)
*/
class HttpResponseSnippet extends TemplatedSnippet {
public class HttpResponseSnippet extends TemplatedSnippet {
HttpResponseSnippet() {
/**
* Creates a new {@code HttpResponseSnippet} with no additional attributes.
*/
protected HttpResponseSnippet() {
this(null);
}
HttpResponseSnippet(Map<String, Object> attributes) {
/**
* Creates a new {@code HttpResponseSnippet} with the given additional
* {@code attributes} that will be included in the model during template rendering.
*
* @param attributes The additional attributes
*/
protected HttpResponseSnippet(Map<String, Object> attributes) {
super("http-response", attributes);
}
@Override
public Map<String, Object> createModel(Operation operation) throws IOException {
protected Map<String, Object> createModel(Operation operation) throws IOException {
OperationResponse response = operation.getResponse();
HttpStatus status = response.getStatus();
Map<String, Object> model = new HashMap<String, Object>();

View File

@@ -67,8 +67,8 @@ public abstract class HypermediaDocumentation {
*/
public static Snippet links(Map<String, Object> attributes,
LinkDescriptor... descriptors) {
return new LinksSnippet(new ContentTypeLinkExtractor(), attributes,
Arrays.asList(descriptors));
return new LinksSnippet(new ContentTypeLinkExtractor(), Arrays.asList(descriptors),
attributes);
}
/**
@@ -98,8 +98,8 @@ public abstract class HypermediaDocumentation {
*/
public static Snippet links(LinkExtractor linkExtractor,
Map<String, Object> attributes, LinkDescriptor... descriptors) {
return new LinksSnippet(linkExtractor, attributes,
Arrays.asList(descriptors));
return new LinksSnippet(linkExtractor, Arrays.asList(descriptors),
attributes);
}
/**

View File

@@ -37,8 +37,12 @@ import org.springframework.util.Assert;
* A {@link Snippet} that documents a RESTful resource's links.
*
* @author Andy Wilkinson
* @see HypermediaDocumentation#links(LinkDescriptor...)
* @see HypermediaDocumentation#links(LinkExtractor, LinkDescriptor...)
* @see HypermediaDocumentation#links(Map, LinkDescriptor...)
* @see HypermediaDocumentation#links(LinkExtractor, Map, LinkDescriptor...)
*/
class LinksSnippet extends TemplatedSnippet {
public class LinksSnippet extends TemplatedSnippet {
private final Map<String, LinkDescriptor> descriptorsByRel = new LinkedHashMap<>();
@@ -46,12 +50,28 @@ class LinksSnippet extends TemplatedSnippet {
private final LinkExtractor linkExtractor;
LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descriptors) {
this(linkExtractor, null, descriptors);
/**
* Creates a new {@code LinksSnippet} that will extract links using the given
* {@code linkExtractor} and document them using the given {@code descriptors}.
*
* @param linkExtractor the link extractor
* @param descriptors the link descriptors
*/
protected LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descriptors) {
this(linkExtractor, descriptors, null);
}
LinksSnippet(LinkExtractor linkExtractor, Map<String, Object> attributes,
List<LinkDescriptor> descriptors) {
/**
* Creates a new {@code LinksSnippet} that will extract links using the given
* {@code linkExtractor} and document them using the given {@code descriptors}. The
* given {@code attributes} will be included in the model during template rendering.
*
* @param linkExtractor the link extractor
* @param descriptors the link descriptors
* @param attributes the additional attributes
*/
protected LinksSnippet(LinkExtractor linkExtractor, List<LinkDescriptor> descriptors,
Map<String, Object> attributes) {
super("links", attributes);
this.linkExtractor = linkExtractor;
for (LinkDescriptor descriptor : descriptors) {

View File

@@ -30,8 +30,8 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link TemplatedSnippet} that produces a snippet documenting a RESTful resource's
* request or response fields.
* Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that
* document a RESTful resource's request or response fields.
*
* @author Andreas Evers
* @author Andy Wilkinson
@@ -40,8 +40,18 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
private List<FieldDescriptor> fieldDescriptors;
AbstractFieldsSnippet(String type, Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
/**
* Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named
* {@code <type>-fields}. The fields will be documented using the given
* {@code descriptors} and the given {@code attributes} will be included in the model
* during template rendering.
*
* @param type the type of the fields
* @param descriptors the field descriptors
* @param attributes the additional attributes
*/
protected AbstractFieldsSnippet(String type, List<FieldDescriptor> descriptors,
Map<String, Object> attributes) {
super(type + "-fields", attributes);
for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath());
@@ -106,8 +116,23 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
}
}
/**
* Returns the content type of the request or response extracted from the given
* {@code operation}.
*
* @param operation The operation
* @return The content type
*/
protected abstract MediaType getContentType(Operation operation);
/**
* Returns the content of the request or response extracted form the given
* {@code operation}.
*
* @param operation The operation
* @return The content
* @throws IOException if the content cannot be extracted
*/
protected abstract byte[] getContent(Operation operation) throws IOException;
}

View File

@@ -136,7 +136,7 @@ public abstract class PayloadDocumentation {
*/
public static Snippet requestFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
return new RequestFieldsSnippet(attributes, Arrays.asList(descriptors));
return new RequestFieldsSnippet(Arrays.asList(descriptors), attributes);
}
/**
@@ -177,7 +177,7 @@ public abstract class PayloadDocumentation {
*/
public static Snippet responseFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
return new ResponseFieldsSnippet(attributes, Arrays.asList(descriptors));
return new ResponseFieldsSnippet(Arrays.asList(descriptors), attributes);
}
}

View File

@@ -28,15 +28,32 @@ import org.springframework.restdocs.snippet.Snippet;
* A {@link Snippet} that documents the fields in a request.
*
* @author Andy Wilkinson
* @see PayloadDocumentation#requestFields(FieldDescriptor...)
* @see PayloadDocumentation#requestFields(Map, FieldDescriptor...)
*/
class RequestFieldsSnippet extends AbstractFieldsSnippet {
public class RequestFieldsSnippet extends AbstractFieldsSnippet {
RequestFieldsSnippet(List<FieldDescriptor> descriptors) {
this(null, descriptors);
/**
* Creates a new {@code RequestFieldsSnippet} that will document the fields in the
* request using the given {@code descriptors}.
*
* @param descriptors the descriptors
*/
protected RequestFieldsSnippet(List<FieldDescriptor> descriptors) {
this(descriptors, null);
}
RequestFieldsSnippet(Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
super("request", attributes, descriptors);
/**
* Creates a new {@code RequestFieldsSnippet} that will document the fields in the
* request using the given {@code descriptors}. The given {@code attributes} will be
* included in the model during template rendering.
*
* @param descriptors the descriptors
* @param attributes the additional attributes
*/
protected RequestFieldsSnippet(List<FieldDescriptor> descriptors,
Map<String, Object> attributes) {
super("request", descriptors, attributes);
}
@Override

View File

@@ -27,16 +27,32 @@ import org.springframework.restdocs.snippet.Snippet;
* A {@link Snippet} that documents the fields in a response.
*
* @author Andy Wilkinson
* @see PayloadDocumentation#responseFields(FieldDescriptor...)
* @see PayloadDocumentation#responseFields(Map, FieldDescriptor...)
*/
class ResponseFieldsSnippet extends AbstractFieldsSnippet {
public class ResponseFieldsSnippet extends AbstractFieldsSnippet {
ResponseFieldsSnippet(List<FieldDescriptor> descriptors) {
this(null, descriptors);
/**
* Creates a new {@code ResponseFieldsSnippet} that will document the fields in the
* response using the given {@code descriptors}.
*
* @param descriptors the descriptors
*/
protected ResponseFieldsSnippet(List<FieldDescriptor> descriptors) {
this(descriptors, null);
}
ResponseFieldsSnippet(Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
super("response", attributes, descriptors);
/**
* Creates a new {@code ResponseFieldsSnippet} that will document the fields in the
* response using the given {@code descriptors}. The given {@code attributes} will be
* included in the model during template rendering.
*
* @param descriptors the descriptors
* @param attributes the additional attributes
*/
protected ResponseFieldsSnippet(List<FieldDescriptor> descriptors,
Map<String, Object> attributes) {
super("response", descriptors, attributes);
}
@Override

View File

@@ -30,12 +30,29 @@ import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.snippet.TemplatedSnippet;
import org.springframework.util.Assert;
abstract class AbstractParametersSnippet extends TemplatedSnippet {
/**
* Abstract {@link TemplatedSnippet} subclass that provides a base for snippets that
* document parameters from a request sent to a RESTful resource.
*
* @author Andreas Evers
* @author Andy Wilkinson
*/
public abstract class AbstractParametersSnippet extends TemplatedSnippet {
private final Map<String, ParameterDescriptor> descriptorsByName = new LinkedHashMap<>();
/**
* Creates a new {@code AbstractParametersSnippet} that will produce a snippet with
* the given {@code snippetName} that will document parameters using the given
* {@code descriptors}. The given {@code attributes} will be included in the model
* during template rendering.
*
* @param snippetName The snippet name
* @param descriptors The descriptors
* @param attributes The additional attributes
*/
protected AbstractParametersSnippet(String snippetName,
Map<String, Object> attributes, List<ParameterDescriptor> descriptors) {
List<ParameterDescriptor> descriptors, Map<String, Object> attributes) {
super(snippetName, attributes);
for (ParameterDescriptor descriptor : descriptors) {
Assert.hasText(descriptor.getName());

View File

@@ -33,28 +33,44 @@ import org.springframework.util.Assert;
* A {@link Snippet} that documents the path parameters supported by a RESTful resource.
*
* @author Andy Wilkinson
* @see RequestDocumentation#pathParameters(ParameterDescriptor...)
* @see RequestDocumentation#pathParameters(Map, ParameterDescriptor...)
*/
class PathParametersSnippet extends AbstractParametersSnippet {
public class PathParametersSnippet extends AbstractParametersSnippet {
private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
PathParametersSnippet(List<ParameterDescriptor> descriptors) {
this(null, descriptors);
/**
* Creates a new {@code PathParametersSnippet} that will document the request's path
* parameters using the given {@code descriptors}.
*
* @param descriptors the parameter descriptors
*/
protected PathParametersSnippet(List<ParameterDescriptor> descriptors) {
this(descriptors, null);
}
PathParametersSnippet(Map<String, Object> attributes,
List<ParameterDescriptor> descriptors) {
super("path-parameters", attributes, descriptors);
/**
* Creates a new {@code PathParametersSnippet} that will document the request's path
* parameters using the given {@code descriptors}. The given {@code attributes} will
* be included in the model during template rendering.
*
* @param descriptors the parameter descriptors
* @param attributes the additional attributes
*/
protected PathParametersSnippet(List<ParameterDescriptor> descriptors,
Map<String, Object> attributes) {
super("path-parameters", descriptors, attributes);
}
@Override
protected Map<String, Object> createModel(Operation operation) throws IOException {
Map<String, Object> model = super.createModel(operation);
model.put("path", remoteQueryStringIfPresent(extractUrlTemplate(operation)));
model.put("path", removeQueryStringIfPresent(extractUrlTemplate(operation)));
return model;
}
private String remoteQueryStringIfPresent(String urlTemplate) {
private String removeQueryStringIfPresent(String urlTemplate) {
int index = urlTemplate.indexOf('?');
if (index == -1) {
return urlTemplate;

View File

@@ -71,7 +71,7 @@ public abstract class RequestDocumentation {
*/
public static Snippet pathParameters(Map<String, Object> attributes,
ParameterDescriptor... descriptors) {
return new PathParametersSnippet(attributes, Arrays.asList(descriptors));
return new PathParametersSnippet(Arrays.asList(descriptors), attributes);
}
/**
@@ -100,7 +100,7 @@ public abstract class RequestDocumentation {
*/
public static Snippet requestParameters(Map<String, Object> attributes,
ParameterDescriptor... descriptors) {
return new RequestParametersSnippet(attributes, Arrays.asList(descriptors));
return new RequestParametersSnippet(Arrays.asList(descriptors), attributes);
}
}

View File

@@ -20,32 +20,45 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletRequest;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.operation.OperationRequest;
import org.springframework.restdocs.snippet.Snippet;
import org.springframework.restdocs.snippet.SnippetException;
import org.springframework.web.bind.annotation.RequestParam;
/**
* A {@link Snippet} that documents the request parameters supported by a RESTful
* resource.
* <p>
* Request parameters are sent as part of the query string or as posted from data.
* Request parameters are sent as part of the query string or as POSTed form data.
*
* @author Andy Wilkinson
* @see ServletRequest#getParameterMap()
* @see RequestParam
* @see OperationRequest#getParameters()
* @see RequestDocumentation#requestParameters(ParameterDescriptor...)
* @see RequestDocumentation#requestParameters(Map, ParameterDescriptor...)
*/
class RequestParametersSnippet extends AbstractParametersSnippet {
public class RequestParametersSnippet extends AbstractParametersSnippet {
RequestParametersSnippet(List<ParameterDescriptor> descriptors) {
this(null, descriptors);
/**
* Creates a new {@code RequestParametersSnippet} that will document the request's
* parameters using the given {@code descriptors}.
*
* @param descriptors the parameter descriptors
*/
protected RequestParametersSnippet(List<ParameterDescriptor> descriptors) {
this(descriptors, null);
}
RequestParametersSnippet(Map<String, Object> attributes,
List<ParameterDescriptor> descriptors) {
super("request-parameters", attributes, descriptors);
/**
* Creates a new {@code RequestParametersSnippet} that will document the request's
* parameters using the given {@code descriptors}. The given {@code attributes} will
* be included in the model during template rendering.
*
* @param descriptors the parameter descriptors
* @param attributes the additional attributes
*/
protected RequestParametersSnippet(List<ParameterDescriptor> descriptors,
Map<String, Object> attributes) {
super("request-parameters", descriptors, attributes);
}
@Override

View File

@@ -38,6 +38,14 @@ public abstract class TemplatedSnippet implements Snippet {
private final String snippetName;
/**
* Creates a new {@code TemplatedSnippet} that will produce a snippet with the given
* {@code snippetName}. The given {@code attributes} will be included in the model
* during rendering of the template.
*
* @param snippetName The name of the snippet
* @param attributes The additional attributes
*/
protected TemplatedSnippet(String snippetName, Map<String, Object> attributes) {
this.snippetName = snippetName;
if (attributes != null) {

View File

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

View File

@@ -154,10 +154,10 @@ public class LinksSnippetTests {
new FileSystemResource(
"src/test/resources/custom-snippet-templates/links-with-title.snippet"));
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("a", "alpha"),
new Link("b", "bravo")), attributes(key("title").value(
"Title for the links")), Arrays.asList(
new Link("b", "bravo")), Arrays.asList(
new LinkDescriptor("a").description("one"),
new LinkDescriptor("b").description("two")))
new LinkDescriptor("b").description("two")), attributes(key("title").value(
"Title for the links")))
.document(new OperationBuilder("links-with-custom-attributes",
this.snippet.getOutputDirectory()).attribute(
TemplateEngine.class.getName(),

View File

@@ -170,8 +170,8 @@ public class RequestFieldsSnippetTests {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("request-fields")).thenReturn(
snippetResource("request-fields-with-title"));
new RequestFieldsSnippet(attributes(key("title").value("Custom title")),
Arrays.asList(fieldWithPath("a").description("one")))
new RequestFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")),
attributes(key("title").value("Custom title")))
.document(new OperationBuilder("request-fields-with-custom-attributes",
this.snippet.getOutputDirectory())
.attribute(TemplateEngine.class.getName(),

View File

@@ -139,8 +139,8 @@ public class ResponseFieldsSnippetTests {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
when(resolver.resolveTemplateResource("response-fields")).thenReturn(
snippetResource("response-fields-with-title"));
new ResponseFieldsSnippet(attributes(key("title").value("Custom title")),
Arrays.asList(fieldWithPath("a").description("one")))
new ResponseFieldsSnippet(Arrays.asList(fieldWithPath("a").description("one")),
attributes(key("title").value("Custom title")))
.document(new OperationBuilder("response-fields-with-custom-attributes",
this.snippet.getOutputDirectory())
.attribute(TemplateEngine.class.getName(),

View File

@@ -147,11 +147,11 @@ public class PathParametersSnippetTests {
when(resolver.resolveTemplateResource("path-parameters")).thenReturn(
snippetResource("path-parameters-with-title"));
new PathParametersSnippet(
attributes(key("title").value("The title")),
Arrays.asList(
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")), parameterWithName("b")
.description("two").attributes(key("foo").value("bravo"))))
.description("two").attributes(key("foo").value("bravo"))),
attributes(key("title").value("The title")))
.document(new OperationBuilder("path-parameters-with-custom-attributes",
this.snippet.getOutputDirectory())
.attribute("org.springframework.restdocs.urlTemplate", "/{a}/{b}")

View File

@@ -131,11 +131,11 @@ public class RequestParametersSnippetTests {
when(resolver.resolveTemplateResource("request-parameters")).thenReturn(
snippetResource("request-parameters-with-title"));
new RequestParametersSnippet(
attributes(key("title").value("The title")),
Arrays.asList(
parameterWithName("a").description("one").attributes(
key("foo").value("alpha")), parameterWithName("b")
.description("two").attributes(key("foo").value("bravo"))))
.description("two").attributes(key("foo").value("bravo"))),
attributes(key("title").value("The title")))
.document(new OperationBuilder(
"request-parameters-with-custom-attributes", this.snippet
.getOutputDirectory())

View File

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