From 70824aa50948625bf632036c21389c3716e2ce32 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 23 Jul 2015 18:01:57 +0100 Subject: [PATCH] Allow documentation of extra attributes on fields, links, and query parameters This commit adds support for associating custom attributes with field, link, and query parameter descriptors. These attributes are then included in the model during snippet rendering. Coupled with a custom snippet template, this enables the inclusion of extra column(s) in the generated tables. Closes gh-70 --- .../docs/asciidoc/customizing-snippets.adoc | 12 ---- .../docs/asciidoc/documenting-your-api.adoc | 63 ++++++++++++++++++- docs/src/docs/asciidoc/index.adoc | 1 - docs/src/test/java/com/example/Payload.java | 15 +++++ .../restdocs/AbstractDescriptor.java | 39 ++++++++++++ .../restdocs/hypermedia/LinkDescriptor.java | 16 ++++- .../hypermedia/LinkSnippetResultHandler.java | 14 ++++- .../restdocs/payload/FieldDescriptor.java | 17 ++++- .../payload/FieldSnippetResultHandler.java | 39 +++++------- .../restdocs/request/ParameterDescriptor.java | 15 ++++- .../QueryParametersSnippetResultHandler.java | 11 +++- .../HypermediaDocumentationTests.java | 30 +++++++++ .../payload/PayloadDocumentationTests.java | 56 +++++++++++++++++ .../request/RequestDocumentationTests.java | 29 +++++++++ .../restdocs/test/StubMvcResult.java | 11 +++- .../links-with-extra-column.snippet | 10 +++ ...query-parameters-with-extra-column.snippet | 10 +++ .../request-fields-with-extra-column.snippet | 11 ++++ .../response-fields-with-extra-column.snippet | 11 ++++ 19 files changed, 364 insertions(+), 46 deletions(-) delete mode 100644 docs/src/docs/asciidoc/customizing-snippets.adoc create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet create mode 100644 spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet diff --git a/docs/src/docs/asciidoc/customizing-snippets.adoc b/docs/src/docs/asciidoc/customizing-snippets.adoc deleted file mode 100644 index 94cca79d..00000000 --- a/docs/src/docs/asciidoc/customizing-snippets.adoc +++ /dev/null @@ -1,12 +0,0 @@ -[[customizing-snippets]] -== Customizing the generated snippets - -Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the -generated snippets. You can customize the generated snippets by overriding the -{source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates[default -templates]. - -Templates are loaded from the classpath in the `org.springframework.restdocs.templates` -package and each template is named after the snippet that it will produce. For example, to -override the template for the `curl-request.adoc` snippet, create a template named -`curl-request.snippet` in `src/test/resources/org/springframework/restdocs/templates`. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index a7652ed4..edac1e2d 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -257,4 +257,65 @@ include::{examples-dir}/com/example/AlwaysDo.java[tags=always-do] With this configuration in place, every call to `MockMvc.perform` will produce the <> without any further configuration. Take a look at the `GettingStartedDocumentation` classes in each of the -sample applications to see this functionality in action. \ No newline at end of file +sample applications to see this functionality in action. + + + +[[documenting-your-api-customizing]] +=== Customizing the output + + + +[[documenting-your-api-customizing-snippets]] +==== Customizing the generated snippets +Spring REST Docs uses https://mustache.github.io[Mustache] templates to produce the +generated snippets. +{source}spring-restdocs/src/main/resources/org/springframework/restdocs/templates[Default +templates] are provided for each of the snippets that Spring REST Docs can produce. To +customize a snippet's content, you can provide your own template. + +Templates are loaded from the classpath in the `org.springframework.restdocs.templates` +package and each template is named after the snippet that it will produce. For example, to +override the template for the `curl-request.adoc` snippet, create a template named +`curl-request.snippet` in `src/test/resources/org/springframework/restdocs/templates`. + + + +[[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: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=constraints] +---- + +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: + +[source,indent=0] +---- + |=== + |Path|Type|Description|Constraints <1> + + {{#fields}} + |{{path}} + |{{type}} + |{{description}} + |{{constraints}} <2> + + {{/fields}} + |=== +---- +<1> Add a new column named "Constraints" +<2> Include the descriptors' `constraints` attribute in each row of the table + + diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index f6afcf0e..2fb04b36 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -24,7 +24,6 @@ include::introduction.adoc[] include::getting-started.adoc[] include::documenting-your-api.adoc[] include::customizing-responses.adoc[] -include::customizing-snippets.adoc[] include::configuration.adoc[] include::working-with-asciidoctor.adoc[] include::contributing.adoc[] \ No newline at end of file diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java index bec0f361..7fcd725a 100644 --- a/docs/src/test/java/com/example/Payload.java +++ b/docs/src/test/java/com/example/Payload.java @@ -19,6 +19,7 @@ package com.example; import static org.springframework.restdocs.RestDocumentation.document; 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 org.springframework.http.MediaType; @@ -51,4 +52,18 @@ private MockMvc mockMvc; // end::explicit-type[] } + public void constraints() throws Exception { + this.mockMvc.perform(post("/users/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + // tag::constraints[] + .andDo(document("create-user").withRequestFields( + fieldWithPath("name") + .description("The user's name") + .attribute("constraints", "Must not be null. Must not be empty"), + fieldWithPath("email") + .description("The user's email address") + .attribute("constrains", "Must be a valid email address"))); + // end::constraints[] + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java new file mode 100644 index 00000000..c87637ff --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/AbstractDescriptor.java @@ -0,0 +1,39 @@ +package org.springframework.restdocs; + +import java.util.HashMap; +import java.util.Map; + +/** + * Base class for descriptors. Provides the ability to associate arbitrary attributes with + * a descriptor. + * + * @author Andy Wilkinson + * + * @param the type of the descriptor + */ +public abstract class AbstractDescriptor> { + + private final Map attributes = new HashMap<>(); + + /** + * Sets an attribute with the given {@code name} and {@code value}. + * + * @param name The name of the attribute + * @param value The value of the attribute + */ + @SuppressWarnings("unchecked") + public T attribute(String name, Object value) { + this.attributes.put(name, value); + return (T) this; + } + + /** + * Returns the descriptor's attributes + * + * @return the attributes + */ + protected Map getAttributes() { + return this.attributes; + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java index dca8adcb..2c1451dc 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkDescriptor.java @@ -16,6 +16,11 @@ package org.springframework.restdocs.hypermedia; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.restdocs.AbstractDescriptor; + /** * A description of a link found in a hypermedia API * @@ -23,7 +28,7 @@ package org.springframework.restdocs.hypermedia; * * @author Andy Wilkinson */ -public class LinkDescriptor { +public class LinkDescriptor extends AbstractDescriptor { private final String rel; @@ -67,4 +72,13 @@ public class LinkDescriptor { boolean isOptional() { return this.optional; } + + Map toModel() { + Map model = new HashMap<>(); + model.put("rel", this.rel); + model.put("description", this.description); + model.put("optional", this.optional); + model.putAll(getAttributes()); + return model; + } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java index 6d18c625..06b239dd 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/hypermedia/LinkSnippetResultHandler.java @@ -18,11 +18,13 @@ package org.springframework.restdocs.hypermedia; import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.springframework.restdocs.snippet.SnippetGenerationException; @@ -34,7 +36,7 @@ import org.springframework.util.Assert; /** * A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful * resource's links. - * + * * @author Andy Wilkinson */ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { @@ -114,8 +116,16 @@ public class LinkSnippetResultHandler extends SnippetWritingResultHandler { TemplateEngine templateEngine = (TemplateEngine) result.getRequest() .getAttribute(TemplateEngine.class.getName()); Map context = new HashMap<>(); - context.put("links", this.descriptorsByRel.values()); + context.put("links", createLinksModel()); writer.print(templateEngine.compileTemplate("links").render(context)); } + private List> createLinksModel() { + List> model = new ArrayList<>(); + for (Entry entry : this.descriptorsByRel.entrySet()) { + model.add(entry.getValue().toModel()); + } + return model; + } + } \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 2c81b9ed..be485d62 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -16,6 +16,11 @@ package org.springframework.restdocs.payload; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.restdocs.AbstractDescriptor; + /** * A description of a field found in a request or response payload * @@ -24,7 +29,7 @@ package org.springframework.restdocs.payload; * @author Andreas Evers * @author Andy Wilkinson */ -public class FieldDescriptor { +public class FieldDescriptor extends AbstractDescriptor { private final String path; @@ -86,4 +91,14 @@ public class FieldDescriptor { String getDescription() { return this.description; } + + Map toModel() { + Map model = new HashMap(); + model.put("path", this.path); + model.put("type", this.type.toString()); + model.put("description", this.description); + model.put("optional", this.optional); + model.putAll(this.getAttributes()); + return model; + } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java index 1e87a551..3ad66c08 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -73,39 +73,32 @@ public abstract class FieldSnippetResultHandler extends SnippetWritingResultHand Object payload = extractPayload(result); - TemplateEngine templateEngine = (TemplateEngine) result.getRequest() - .getAttribute(TemplateEngine.class.getName()); Map context = new HashMap<>(); - List> fields = new ArrayList<>(); + List> fields = new ArrayList<>(); context.put("fields", fields); for (Entry entry : this.descriptorsByPath.entrySet()) { FieldDescriptor descriptor = entry.getValue(); - FieldType type = getFieldType(descriptor, payload); - Map fieldModel = new HashMap<>(); - fieldModel.put("path", entry.getKey()); - fieldModel.put("type", type.toString()); - fieldModel.put("description", descriptor.getDescription()); - fields.add(fieldModel); + if (descriptor.getType() == null) { + descriptor.type(getFieldType(descriptor, payload)); + } + fields.add(descriptor.toModel()); } + TemplateEngine templateEngine = (TemplateEngine) result.getRequest() + .getAttribute(TemplateEngine.class.getName()); writer.print(templateEngine.compileTemplate(this.templateName).render(context)); } private FieldType getFieldType(FieldDescriptor descriptor, Object payload) { - if (descriptor.getType() != null) { - return descriptor.getType(); + try { + return FieldSnippetResultHandler.this.fieldTypeResolver.resolveFieldType( + descriptor.getPath(), payload); } - else { - try { - return FieldSnippetResultHandler.this.fieldTypeResolver.resolveFieldType( - descriptor.getPath(), payload); - } - catch (FieldDoesNotExistException ex) { - String message = "Cannot determine the type of the field '" - + descriptor.getPath() + "' as it is not present in the" - + " payload. Please provide a type using" - + " FieldDescriptor.type(FieldType)."; - throw new FieldTypeRequiredException(message); - } + catch (FieldDoesNotExistException ex) { + String message = "Cannot determine the type of the field '" + + descriptor.getPath() + "' as it is not present in the" + + " payload. Please provide a type using" + + " FieldDescriptor.type(FieldType)."; + throw new FieldTypeRequiredException(message); } } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java index 63a1be58..abc4ee0b 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/ParameterDescriptor.java @@ -16,6 +16,11 @@ package org.springframework.restdocs.request; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.restdocs.AbstractDescriptor; + /** * A descriptor of a parameter in a query string * @@ -23,7 +28,7 @@ package org.springframework.restdocs.request; * @see RequestDocumentation#parameterWithName * */ -public class ParameterDescriptor { +public class ParameterDescriptor extends AbstractDescriptor { private final String name; @@ -52,4 +57,12 @@ public class ParameterDescriptor { return this.description; } + Map toModel() { + Map model = new HashMap<>(); + model.put("name", this.name); + model.put("description", this.description); + model.putAll(getAttributes()); + return model; + } + } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java index 8813d0c8..661e80ff 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/request/QueryParametersSnippetResultHandler.java @@ -18,10 +18,13 @@ package org.springframework.restdocs.request; import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.springframework.restdocs.snippet.SnippetGenerationException; @@ -33,7 +36,7 @@ import org.springframework.util.Assert; /** * A {@link SnippetWritingResultHandler} that produces a snippet documenting the query * parameters supported by a RESTful resource. - * + * * @author Andy Wilkinson */ public class QueryParametersSnippetResultHandler extends SnippetWritingResultHandler { @@ -90,7 +93,11 @@ public class QueryParametersSnippetResultHandler extends SnippetWritingResultHan TemplateEngine templateEngine = (TemplateEngine) result.getRequest() .getAttribute(TemplateEngine.class.getName()); Map context = new HashMap<>(); - context.put("parameters", this.descriptorsByName.values()); + List> parameters = new ArrayList<>(); + for (Entry entry : this.descriptorsByName.entrySet()) { + parameters.add(entry.getValue().toModel()); + } + context.put("parameters", parameters); writer.print(templateEngine.compileTemplate("query-parameters").render(context)); } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java index 56588a06..e282f2b9 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/hypermedia/HypermediaDocumentationTests.java @@ -17,6 +17,8 @@ package org.springframework.restdocs.hypermedia; import static org.hamcrest.CoreMatchers.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; import static org.springframework.restdocs.test.StubMvcResult.result; @@ -26,8 +28,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.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.snippet.SnippetGenerationException; +import org.springframework.restdocs.templates.TemplateEngine; +import org.springframework.restdocs.templates.TemplateResourceResolver; +import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine; import org.springframework.restdocs.test.ExpectedSnippet; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -107,6 +114,29 @@ public class HypermediaDocumentationTests { new LinkDescriptor("b").description("two")).handle(result()); } + @Test + public void linksWithCustomAttributes() throws IOException { + this.snippet.expectLinks("documented-links").withContents( // + tableWithHeader("Relation", "Description", "Foo") // + .row("a", "one", "alpha") // + .row("b", "two", "bravo")); + MockHttpServletRequest request = new MockHttpServletRequest(); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("links")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/links-with-extra-column.snippet")); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + resolver)); + documentLinks( + "documented-links", + 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)); + } + private static class StubLinkExtractor implements LinkExtractor { private MultiValueMap linksByRel = new LinkedMultiValueMap(); diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java index c96afe0d..b788da69 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/PayloadDocumentationTests.java @@ -18,6 +18,8 @@ package org.springframework.restdocs.payload; import static org.hamcrest.CoreMatchers.endsWith; 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.payload.PayloadDocumentation.documentRequestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -30,8 +32,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.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.restdocs.snippet.SnippetGenerationException; +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; /** @@ -174,4 +181,53 @@ public class PayloadDocumentationTests { 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")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("fields")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet")); + MockHttpServletRequest request = new MockHttpServletRequest(); + 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)); + } + + @Test + public void responseFieldsWithCustomAttributes() throws IOException { + this.snippet.expectResponseFields("response-fields-with-custom-attributes") + .withContents( // + tableWithHeader("Path", "Type", "Description", "Foo") // + .row("a.b", "Number", "one", "alpha") // + .row("a.c", "String", "two", "bravo") // + .row("a", "Object", "three", "charlie")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("fields")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet")); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAttribute(TemplateEngine.class.getName(), new MustacheTemplateEngine( + 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)); + } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java index 32c794b8..f3a87d6b 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/request/RequestDocumentationTests.java @@ -17,6 +17,8 @@ package org.springframework.restdocs.request; import static org.hamcrest.CoreMatchers.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.request.RequestDocumentation.documentQueryParameters; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.test.SnippetMatchers.tableWithHeader; @@ -28,7 +30,12 @@ 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.mock.web.MockHttpServletRequest; import org.springframework.restdocs.snippet.SnippetGenerationException; +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; /** @@ -100,4 +107,26 @@ public class RequestDocumentationTests { result(get("/?a=alpha&b=bravo"))); } + @Test + public void parametersWithCustomAttributes() throws IOException { + this.snippet.expectQueryParameters("parameters-with-custom-attributes") + .withContents( + tableWithHeader("Parameter", "Description", "Foo").row("a", + "one", "alpha").row("b", "two", "bravo")); + TemplateResourceResolver resolver = mock(TemplateResourceResolver.class); + when(resolver.resolveTemplateResource("query-parameters")) + .thenReturn( + new FileSystemResource( + "src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.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", + parameterWithName("a").description("one").attribute("foo", "alpha"), + parameterWithName("b").description("two").attribute("foo", "bravo")) + .handle(result(request)); + + } } diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java index 39ef530d..7b1d3150 100644 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/test/StubMvcResult.java @@ -61,6 +61,11 @@ public class StubMvcResult implements MvcResult { return new StubMvcResult(response); } + public static StubMvcResult result(MockHttpServletRequest request, + MockHttpServletResponse response) { + return new StubMvcResult(request, response); + } + private StubMvcResult() { this(new MockHttpServletRequest(), new MockHttpServletResponse()); } @@ -79,8 +84,10 @@ public class StubMvcResult implements MvcResult { private StubMvcResult(MockHttpServletRequest request, MockHttpServletResponse response) { this.request = request; - this.request.setAttribute(TemplateEngine.class.getName(), - new MustacheTemplateEngine(new StandardTemplateResourceResolver())); + if (this.request.getAttribute(TemplateEngine.class.getName()) == null) { + this.request.setAttribute(TemplateEngine.class.getName(), + new MustacheTemplateEngine(new StandardTemplateResourceResolver())); + } this.response = response; } diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet new file mode 100644 index 00000000..81c8232c --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/links-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Relation|Description|Foo + +{{#links}} +|{{rel}} +|{{description}} +|{{foo}} + +{{/links}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet new file mode 100644 index 00000000..fb97f89b --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/query-parameters-with-extra-column.snippet @@ -0,0 +1,10 @@ +|=== +|Parameter|Description|Foo + +{{#parameters}} +|{{name}} +|{{description}} +|{{foo}} + +{{/parameters}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet new file mode 100644 index 00000000..4830d895 --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/request-fields-with-extra-column.snippet @@ -0,0 +1,11 @@ +|=== +|Path|Type|Description|Foo + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} +|{{foo}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet b/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet new file mode 100644 index 00000000..4830d895 --- /dev/null +++ b/spring-restdocs/src/test/resources/custom-snippet-templates/response-fields-with-extra-column.snippet @@ -0,0 +1,11 @@ +|=== +|Path|Type|Description|Foo + +{{#fields}} +|{{path}} +|{{type}} +|{{description}} +|{{foo}} + +{{/fields}} +|=== \ No newline at end of file