diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index 2dee3919..f798b34a 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -296,6 +296,85 @@ method will be used in the documentation. +[[documenting-your-api-request-response-payloads-reusing-field-descriptors]] +==== Reusing field descriptors + +In addition to the general support for <>, the request and response snippets allow additional descriptors to be +configured with a path prefix. This allows the descriptors for a repeated portion of a +request or response payload to be created once and then reused. + +Consider an endpoint that returns a book: + +[source,json,indent=0] +---- + { + "title": "Pride and Prejudice", + "author": "Jane Austen" + } +---- + +The paths for `title` and `author` are simply `title` and `author` respectively. + +Now consider an endpoint that returns an array of books: + +[source,json,indent=0] +---- + [{ + "title": "Pride and Prejudice", + "author": "Jane Austen" + }, + { + "title": "To Kill a Mockingbird", + "author": "Harper Lee" + }] +---- + +The paths for `title` and `author` are `[].title` and `[].author` respectively. The only +difference between the single book and the array of books is that the fields' paths now +have a `[].` prefix. + +The descriptors that document a book can be created: + +[source,java,indent=0] +---- +include::{examples-dir}/com/example/Payload.java[tags=book-descriptors] +---- + +They can then be used to document a single book: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=single-book] +---- +<1> Document `title` and `author` using existing descriptors + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=single-book] +---- +<1> Document `title` and `author` using existing descriptors + +And an array of books: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/Payload.java[tags=book-array] +---- +<1> Document the array +<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array] +---- +<1> Document the array +<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].` + [[documenting-your-api-request-parameters]] === Request parameters diff --git a/docs/src/test/java/com/example/Payload.java b/docs/src/test/java/com/example/Payload.java new file mode 100644 index 00000000..d33de4d1 --- /dev/null +++ b/docs/src/test/java/com/example/Payload.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014-2016 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 com.example; + +import org.springframework.restdocs.payload.FieldDescriptor; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +public class Payload { + + @SuppressWarnings("unused") + public void bookFieldDescriptors() { + // tag::book-descriptors[] + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + // end::book-descriptors[] + } + +} diff --git a/docs/src/test/java/com/example/mockmvc/Payload.java b/docs/src/test/java/com/example/mockmvc/Payload.java index 4424b1f3..b811c1a6 100644 --- a/docs/src/test/java/com/example/mockmvc/Payload.java +++ b/docs/src/test/java/com/example/mockmvc/Payload.java @@ -27,6 +27,7 @@ import static org.springframework.restdocs.snippet.Attributes.key; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.MockMvc; @@ -70,4 +71,24 @@ public class Payload { // end::constraints[] } + public void descriptorReuse() throws Exception { + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + + // tag::single-book[] + this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("book", responseFields(book))); // <1> + // end::single-book[] + + // tag::book-array[] + this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("book", responseFields( + fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))); // <2> + // end::book-array[] + } + } diff --git a/docs/src/test/java/com/example/restassured/Payload.java b/docs/src/test/java/com/example/restassured/Payload.java index ee6c2456..225eb619 100644 --- a/docs/src/test/java/com/example/restassured/Payload.java +++ b/docs/src/test/java/com/example/restassured/Payload.java @@ -16,6 +16,7 @@ package com.example.restassured; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; import com.jayway.restassured.RestAssured; @@ -72,4 +73,27 @@ public class Payload { .then().assertThat().statusCode(is(200)); } + public void descriptorReuse() throws Exception { + FieldDescriptor[] book = new FieldDescriptor[] { + fieldWithPath("title").description("Title of the book"), + fieldWithPath("author").description("Author of the book") }; + + // tag::single-book[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("book", responseFields(book))) // <1> + .when().get("/books/1") + .then().assertThat().statusCode(is(200)); + // end::single-book[] + + // tag::book-array[] + RestAssured.given(this.spec).accept("application/json") + .filter(document("books", responseFields( + fieldWithPath("[]").description("An array of books")) // <1> + .andWithPrefix("[].", book))) // <2> + .when().get("/books/1") + .then().assertThat().statusCode(is(200)); + // end::book-array[] + + } + } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java index dcff13bd..19322b28 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/AbstractFieldsSnippet.java @@ -180,4 +180,30 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { return model; } + /** + * Creates a copy of the given {@code descriptors} with the given {@code pathPrefix} + * applied to their paths. + * + * @param pathPrefix the path prefix + * @param descriptors the descriptors to copy + * @return the copied descriptors with the prefix applied + */ + protected final List applyPathPrefix(String pathPrefix, + List descriptors) { + List prefixedDescriptors = new ArrayList<>(); + for (FieldDescriptor descriptor : descriptors) { + FieldDescriptor prefixedDescriptor = new FieldDescriptor( + pathPrefix + descriptor.getPath()) + .description(descriptor.getDescription()) + .type(descriptor.getType()); + if (descriptor.isIgnored()) { + prefixedDescriptor.ignored(); + } + if (descriptor.isOptional()) { + prefixedDescriptor.optional(); + } + prefixedDescriptors.add(prefixedDescriptor); + } + return prefixedDescriptors; + } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java index fdc3e609..9e296887 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestFieldsSnippet.java @@ -72,13 +72,30 @@ public class RequestFieldsSnippet extends AbstractFieldsSnippet { * Returns a new {@code RequestFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. + * * @param additionalDescriptors the additional descriptors * @return the new snippet */ public RequestFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public RequestFieldsSnippet andWithPrefix(String pathPrefix, + FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + combinedDescriptors.addAll( + applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new RequestFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java index 14d5dd55..28815c4c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/ResponseFieldsSnippet.java @@ -72,13 +72,30 @@ public class ResponseFieldsSnippet extends AbstractFieldsSnippet { * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's * attributes and its descriptors combined with the given * {@code additionalDescriptors}. + * * @param additionalDescriptors the additional descriptors * @return the new snippet */ public ResponseFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code ResponseFieldsSnippet} configured with this snippet's + * attributes and its descriptors combined with the given + * {@code additionalDescriptors}. The given {@code pathPrefix} is applied to the path + * of each additional descriptor. + * + * @param pathPrefix the prefix to apply to the additional descriptors + * @param additionalDescriptors the additional descriptors + * @return the new snippet + */ + public ResponseFieldsSnippet andWithPrefix(String pathPrefix, + FieldDescriptor... additionalDescriptors) { List combinedDescriptors = new ArrayList<>(); combinedDescriptors.addAll(getFieldDescriptors()); - combinedDescriptors.addAll(Arrays.asList(additionalDescriptors)); + combinedDescriptors.addAll( + applyPathPrefix(pathPrefix, Arrays.asList(additionalDescriptors))); return new ResponseFieldsSnippet(combinedDescriptors, this.getAttributes()); } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java index 195ed58a..79e02a8c 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestFieldsSnippetTests.java @@ -176,4 +176,19 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests { .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); } + @Test + public void prefixedAdditionalDescriptors() throws IOException { + this.snippet.expectRequestFields("prefixed-additional-descriptors") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "Object", "one").row("a.b", "Number", "two") + .row("a.c", "String", "three")); + + PayloadDocumentation.requestFields(fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), + fieldWithPath("c").description("three")) + .document(operationBuilder("prefixed-additional-descriptors") + .request("http://localhost") + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + } + } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java index 8e33fada..ae490e50 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/ResponseFieldsSnippetTests.java @@ -232,4 +232,18 @@ public class ResponseFieldsSnippetTests extends AbstractSnippetTests { .build()); } + @Test + public void prefixedAdditionalDescriptors() throws IOException { + this.snippet.expectResponseFields("prefixed-additional-descriptors") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("a", "Object", "one").row("a.b", "Number", "two") + .row("a.c", "String", "three")); + + PayloadDocumentation.responseFields(fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), + fieldWithPath("c").description("three")) + .document(operationBuilder("prefixed-additional-descriptors").response() + .content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build()); + } + }