diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index d1a27442..4b3d58fa 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -415,7 +415,7 @@ include::{examples-dir}/com/example/mockmvc/RequestParameters.java[tags=request- ---- <1> Perform a `GET` request with two parameters, `page` and `per_page` in the query string. -<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. +<2> Configure Spring REST Docs to produce a snippet describing the request's parameters. Uses the static `requestParameters` method on `org.springframework.restdocs.request.RequestDocumentation`. <3> Document the `page` parameter. Uses the static `parameterWithName` method on @@ -575,6 +575,58 @@ prevent it from appearing in the generated snippet while avoiding the failure de above. +[[documenting-your-api-request-parts-payloads]] +=== Request part payloads + +The payload of a request part can be documented in much the same way as the +<>: + +[source,java,indent=0,role="primary"] +.MockMvc +---- +include::{examples-dir}/com/example/mockmvc/RequestPartPayload.java[tags=payload] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload + of the request part named `metadata`. Uses the static `requestPartFields` method on + `PayloadDocumentation`. + payload. +<2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. + +[source,java,indent=0,role="secondary"] +.REST Assured +---- +include::{examples-dir}/com/example/restassured/RequestPartPayload.java[tags=payload] +---- +<1> Configure Spring REST docs to produce a snippet describing the fields in the payload + of the request part named `metadata`. Uses the static `requestPartFields` method on + `PayloadDocumentation`. +<2> Expect a field with the path `version`. Uses the static `fieldWithPath` method on + `org.springframework.restdocs.payload.PayloadDocumentation`. + +The result is a snippet that contains a table describing the part's fields. This snippet +is named `request-part-${part-name}-fields.adoc`. For example, documenting a part named +`metadata` will produce a snippet named `request-part-metadata-fields.adoc`. + +When documenting fields, the test will fail if an undocumented field is found in the +payload of the part. Similarly, the test will also fail if a documented field is not found +in the payload of the part and the field has not been marked as optional. For payloads +with a hierarchical structure, documenting a field is sufficient for all of its +descendants to also be treated as having been documented. + +If you do not want to document a field, you can mark it as ignored. This will prevent it +from appearing in the generated snippet while avoiding the failure described above. + +Fields can also be documented in a relaxed mode where any undocumented fields will not +cause a test failure. To do so, use the `relaxedRequestPartFields` method on +`org.springframework.restdocs.payload.PayloadDocumentation`. This can be useful when +documenting a particular scenario where you only want to focus on a subset of the payload +of the part. + +For further information on describing fields, documenting payloads that use XML, +and more please refer to the +<>. [[documenting-your-api-http-headers]] === HTTP headers diff --git a/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java new file mode 100644 index 00000000..56d6ae85 --- /dev/null +++ b/docs/src/test/java/com/example/mockmvc/RequestPartPayload.java @@ -0,0 +1,48 @@ +/* + * 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.mockmvc; + +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.fileUpload; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public class RequestPartPayload { + + private MockMvc mockMvc; + + public void response() throws Exception { + // tag::payload[] + MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png", + "<>".getBytes()); + MockMultipartFile metadata = new MockMultipartFile("metadata", "", + "application/json", "{ \"version\": \"1.0\"}".getBytes()); + + this.mockMvc.perform(fileUpload("/images").file(image).file(metadata) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(document("image-upload", requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))); // <2> + // end::payload[] + } + +} diff --git a/docs/src/test/java/com/example/restassured/RequestPartPayload.java b/docs/src/test/java/com/example/restassured/RequestPartPayload.java new file mode 100644 index 00000000..5dc8eda5 --- /dev/null +++ b/docs/src/test/java/com/example/restassured/RequestPartPayload.java @@ -0,0 +1,48 @@ +/* + * 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.restassured; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.specification.RequestSpecification; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestPartFields; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; + +public class RequestPartPayload { + + private RequestSpecification spec; + + public void response() throws Exception { + // tag::payload[] + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + RestAssured.given(this.spec).accept("application/json") + .filter(document("image-upload", requestPartFields("metadata", // <1> + fieldWithPath("version").description("The version of the image")))) // <2> + .when().multiPart("image", new File("image.png"), "image/png") + .multiPart("metadata", metadata).post("images") + .then().assertThat().statusCode(is(200)); + // end::payload[] + } + +} 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 16f7ba61..ae6fb3fe 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 @@ -65,10 +65,11 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { /** * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named - * {@code -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. If {@code ignoreUndocumentedFields} is {@code true}, - * undocumented fields will be ignored and will not trigger a failure. + * {@code -fields} using a template named {@code -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. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. * * @param type the type of the fields * @param descriptors the field descriptors @@ -77,7 +78,27 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet { */ protected AbstractFieldsSnippet(String type, List descriptors, Map attributes, boolean ignoreUndocumentedFields) { - super(type + "-fields", attributes); + this(type, type, descriptors, attributes, ignoreUndocumentedFields); + } + + /** + * Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named + * {@code -fields} using a template named {@code -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. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param name the name of the snippet + * @param type the type of the fields + * @param descriptors the field descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected AbstractFieldsSnippet(String name, String type, + List descriptors, Map attributes, + boolean ignoreUndocumentedFields) { + super(name + "-fields", type + "-fields", attributes); for (FieldDescriptor descriptor : descriptors) { Assert.notNull(descriptor.getPath(), "Field descriptors must have a path"); if (!descriptor.isIgnored()) { diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index c4a57c25..2e1d8ea2 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -268,6 +268,190 @@ public abstract class PayloadDocumentation { return new RequestFieldsSnippet(descriptors, attributes, true); } + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

+ * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

+ * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + FieldDescriptor... descriptors) { + return requestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

+ * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

+ * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

+ * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors}. + *

+ * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors, true); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

+ * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

+ * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + Map attributes, FieldDescriptor... descriptors) { + return requestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

+ * If a field is present in the request part, but is not documented by one of the + * descriptors, a failure will occur when the snippet is invoked. Similarly, if a + * field is documented, is not marked as optional, and is not present in the request + * part, 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. + *

+ * If you do not want to document a field, a field descriptor can be marked as + * {@link FieldDescriptor#ignored}. This will prevent it from appearing in the + * generated snippet while avoiding the failure described above. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet requestPartFields(String part, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

+ * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + Map attributes, FieldDescriptor... descriptors) { + return relaxedRequestPartFields(part, Arrays.asList(descriptors)); + } + + /** + * Returns a {@code Snippet} that will document the fields of the specified + * {@code part} of the API operations's request payload. The fields will be documented + * using the given {@code descriptors} and the given {@code attributes} will be + * available during snippet generation. + *

+ * If a field is documented, is not marked as optional, and is not present in the + * request, a failure will occur. Any undocumented fields will be ignored. + * + * @param part the part name + * @param attributes the attributes + * @param descriptors the descriptions of the request part's fields + * @return the snippet that will document the fields + * @see #fieldWithPath(String) + */ + public static RequestPartFieldsSnippet relaxedRequestPartFields(String part, + Map attributes, List descriptors) { + return new RequestPartFieldsSnippet(part, descriptors, true); + } + /** * Returns a {@code Snippet} that will document the fields of the API operation's * response payload. The fields will be documented using the given {@code descriptors} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java new file mode 100644 index 00000000..f07923de --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -0,0 +1,190 @@ +/* + * 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 org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.http.MediaType; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.snippet.Snippet; +import org.springframework.restdocs.snippet.SnippetException; + +/** + * A {@link Snippet} that documents the fields in a request part. + * + * @author Mathieu Pousse + * @author Andy Wilkinson + * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) + * @see PayloadDocumentation#requestPartFields(String, List) + */ +public class RequestPartFieldsSnippet extends AbstractFieldsSnippet { + + private final String partName; + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the + * request part using the given {@code descriptors}. Undocumented fields will trigger + * a failure. + * + * @param partName the part name + * @param descriptors the descriptors + */ + protected RequestPartFieldsSnippet(String partName, + List descriptors) { + this(partName, descriptors, null, false); + } + + /** + * Creates a new {@code RequestPartFieldsSnippet} that will document the fields in the + * request part using the given {@code descriptors}. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param partName the part name + * @param descriptors the descriptors + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + boolean ignoreUndocumentedFields) { + this(partName, descriptors, null, ignoreUndocumentedFields); + } + + /** + * 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. Undocumented fields will trigger a + * failure. + * + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + Map attributes) { + this(partName, descriptors, attributes, false); + } + + /** + * 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. If + * {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be + * ignored and will not trigger a failure. + * + * @param partName the part name + * @param descriptors the descriptors + * @param attributes the additional attributes + * @param ignoreUndocumentedFields whether undocumented fields should be ignored + */ + protected RequestPartFieldsSnippet(String partName, List descriptors, + Map attributes, boolean ignoreUndocumentedFields) { + super("request-part-" + partName, "request-part", descriptors, attributes, + ignoreUndocumentedFields); + this.partName = partName; + } + + @Override + protected MediaType getContentType(Operation operation) { + return findPart(operation).getHeaders().getContentType(); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + return findPart(operation).getContent(); + } + + private OperationRequestPart findPart(Operation operation) { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate; + } + } + throw new SnippetException("A request part named '" + this.partName + + "' was not found in the request"); + } + + /** + * Returns a new {@code RequestPartFieldsSnippet} 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 final RequestPartFieldsSnippet and(FieldDescriptor... additionalDescriptors) { + return andWithPrefix("", additionalDescriptors); + } + + /** + * Returns a new {@code RequestPartFieldsSnippet} 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 final RequestPartFieldsSnippet and( + List 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 final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, + FieldDescriptor... additionalDescriptors) { + List combinedDescriptors = new ArrayList<>(); + combinedDescriptors.addAll(getFieldDescriptors()); + combinedDescriptors.addAll(PayloadDocumentation.applyPathPrefix(pathPrefix, + Arrays.asList(additionalDescriptors))); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, + this.getAttributes()); + } + + /** + * 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 final RequestPartFieldsSnippet andWithPrefix(String pathPrefix, + List additionalDescriptors) { + List combinedDescriptors = new ArrayList<>( + getFieldDescriptors()); + combinedDescriptors.addAll( + PayloadDocumentation.applyPathPrefix(pathPrefix, additionalDescriptors)); + return new RequestPartFieldsSnippet(this.partName, combinedDescriptors, + this.getAttributes()); + } + +} diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet new file mode 100644 index 00000000..46cd43fe --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor/default-request-part-fields.snippet @@ -0,0 +1,10 @@ +|=== +|Path|Type|Description + +{{#fields}} +|{{#tableCellContent}}`{{path}}`{{/tableCellContent}} +|{{#tableCellContent}}`{{type}}`{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} +|=== \ No newline at end of file diff --git a/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet new file mode 100644 index 00000000..27a4e437 --- /dev/null +++ b/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/markdown/default-request-part-fields.snippet @@ -0,0 +1,5 @@ +Path | Type | Description +---- | ---- | ----------- +{{#fields}} +`{{path}}` | `{{type}}` | {{description}} +{{/fields}} \ No newline at end of file diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java new file mode 100644 index 00000000..03cf666e --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetFailureTests.java @@ -0,0 +1,83 @@ +/* + * 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 org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.restdocs.snippet.SnippetException; +import org.springframework.restdocs.templates.TemplateFormats; +import org.springframework.restdocs.test.OperationBuilder; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.startsWith; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for failures when rendering {@link RequestPartFieldsSnippet} due to missing or + * undocumented fields. + * + * @author Mathieu Pousse + * @author Andy Wilkinson + */ +public class RequestPartFieldsSnippetFailureTests { + + @Rule + public OperationBuilder operationBuilder = new OperationBuilder( + TemplateFormats.asciidoctor()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void undocumentedRequestPartField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); + new RequestPartFieldsSnippet("part", Collections.emptyList()) + .document(this.operationBuilder.request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); + } + + @Test + public void missingRequestPartField() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + startsWith("The following parts of the payload were not documented:")); + new RequestPartFieldsSnippet("part", + Arrays.asList(fieldWithPath("b").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); + } + + @Test + public void missingRequestPart() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage( + equalTo("A request part named 'another' was not found in the request")); + new RequestPartFieldsSnippet("another", + Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(this.operationBuilder.request("http://localhost") + .part("part", "{\"a\": {\"b\": 5}}".getBytes()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java new file mode 100644 index 00000000..0d4c7c32 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartFieldsSnippetTests.java @@ -0,0 +1,118 @@ +/* + * 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 org.springframework.restdocs.payload; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.operation.Operation; +import org.springframework.restdocs.templates.TemplateFormat; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for {@link RequestPartFieldsSnippet}. + * + * @author Mathieu Pousse + * @author Andy Wilkinson + */ +public class RequestPartFieldsSnippetTests extends AbstractSnippetTests { + + public RequestPartFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void mapRequestPartFields() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + + new RequestPartFieldsSnippet("one", + Arrays.asList(fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two"), fieldWithPath("a") + .description("three"))) + .document( + this.operationBuilder + .request("http://localhost") + .part("one", + "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}" + .getBytes()) + .build()); + } + + @Test + public void multipleRequestParts() throws IOException { + this.snippets.expectRequestPartFields("one"); + this.snippets.expectRequestPartFields("two"); + Operation operation = this.operationBuilder.request("http://localhost") + .part("one", "{}".getBytes()).and().part("two", "{}".getBytes()).build(); + new RequestPartFieldsSnippet("one", Collections.emptyList()) + .document(operation); + new RequestPartFieldsSnippet("two", Collections.emptyList()) + .document(operation); + } + + @Test + public void allUndocumentedRequestPartFieldsCanBeIgnored() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description").row("`b`", + "`Number`", "Field b")); + new RequestPartFieldsSnippet("one", + Arrays.asList(fieldWithPath("b").description("Field b")), true) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\": 5, \"b\": 4}".getBytes()).build()); + } + + @Test + public void additionalDescriptors() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + + PayloadDocumentation + .requestPartFields("one", fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two")) + .and(fieldWithPath("a").description("three")) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + } + + @Test + public void prefixedAdditionalDescriptors() throws IOException { + this.snippets.expectRequestPartFields("one") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a`", "`Object`", "one").row("`a.b`", "`Number`", "two") + .row("`a.c`", "`String`", "three")); + + PayloadDocumentation + .requestPartFields("one", fieldWithPath("a").description("one")) + .andWithPrefix("a.", fieldWithPath("b").description("two"), + fieldWithPath("c").description("three")) + .document(this.operationBuilder.request("http://localhost") + .part("one", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java index f57c5b95..3a174532 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/test/ExpectedSnippets.java @@ -81,6 +81,10 @@ public class ExpectedSnippets extends OperationTestRule { return expect("request-fields"); } + public ExpectedSnippet expectRequestPartFields(String partName) { + return expect("request-part-" + partName + "-fields"); + } + public ExpectedSnippet expectResponseFields() { return expect("response-fields"); }