diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc index d1a27442..96117ffd 100644 --- a/docs/src/docs/asciidoc/documenting-your-api.adoc +++ b/docs/src/docs/asciidoc/documenting-your-api.adoc @@ -575,6 +575,12 @@ prevent it from appearing in the generated snippet while avoiding the failure de above. +[[documenting-your-api-request-parts]] +=== Request parts content + +If you need to document the content of request part that holds Json data, you should use`requestPartsFields`. +It acts exactly as [[documenting-your-api-request-parts]] but in order to use it, you must specify the part name. + [[documenting-your-api-http-headers]] === HTTP headers 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..fed726f1 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 @@ -148,6 +148,56 @@ public abstract class PayloadDocumentation { return new RequestFieldsSnippet(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 payload, 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, + * 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 payload'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 payload, 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, + * 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 payload'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 API operations's * request 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..4de8f848 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/payload/RequestPartFieldsSnippet.java @@ -0,0 +1,191 @@ +/* + * 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. + * + * @author Mathieu Pousse + * @see PayloadDocumentation#requestPartFields(String, FieldDescriptor...) + */ +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", descriptors, attributes, ignoreUndocumentedFields); + this.partName = partName; + } + + @Override + protected MediaType getContentType(Operation operation) { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate.getHeaders().getContentType(); + } + } + throw new SnippetException(missingPartErrorMessage()); + } + + @Override + protected byte[] getContent(Operation operation) throws IOException { + for (OperationRequestPart candidate : operation.getRequest().getParts()) { + if (candidate.getName().equals(this.partName)) { + return candidate.getContent(); + } + } + throw new SnippetException(missingPartErrorMessage()); + } + + /** + * Prepare the error message because the requested part was not found. + * + * @return see description + */ + protected String missingPartErrorMessage() { + return "Request parts with the following names were not found in the request: " + this.partName; + } + + /** + * 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/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java new file mode 100644 index 00000000..a025514c --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetFailureTests.java @@ -0,0 +1,82 @@ +/* + * 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.ExpectedSnippet; +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 + */ +public class RequestPartsFieldsSnippetFailureTests { + + @Rule + public ExpectedSnippet snippet = new ExpectedSnippet(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(new OperationBuilder("undocumented-request-field", + this.snippet.getOutputDirectory()).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(new OperationBuilder("undocumented-request-field", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("part", "{\"a\": 5}".getBytes()).build()); + } + + @Test + public void missingRequestPart() throws IOException { + this.thrown.expect(SnippetException.class); + this.thrown.expectMessage(equalTo("Request parts with the following names were not found in the request: another")); + new RequestPartFieldsSnippet("another", Arrays.asList(fieldWithPath("a.b").description("one"))) + .document(new OperationBuilder("missing-request-fields", + this.snippet.getOutputDirectory()).request("http://localhost") + .part("part", "{\"a\": {\"b\": 5}}".getBytes()).build()); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java new file mode 100644 index 00000000..8dbff585 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/payload/RequestPartsFieldsSnippetTests.java @@ -0,0 +1,56 @@ +/* + * 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 org.junit.Test; + +import org.springframework.restdocs.AbstractSnippetTests; +import org.springframework.restdocs.templates.TemplateFormat; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +/** + * Tests for {@link RequestPartFieldsSnippet}. + * + * @author Mathieu Pousse + */ +public class RequestPartsFieldsSnippetTests extends AbstractSnippetTests { + + public RequestPartsFieldsSnippetTests(String name, TemplateFormat templateFormat) { + super(name, templateFormat); + } + + @Test + public void mapRequestWithFields() throws IOException { + this.snippet.expectRequestFields("map-request-parts-with-fields") + .withContents(tableWithHeader("Path", "Type", "Description") + .row("`a.b`", "`Number`", "one").row("`a.c`", "`String`", "two") + .row("`a`", "`Object`", "three")); + + new RequestPartFieldsSnippet("part", Arrays.asList(fieldWithPath("a.b").description("one"), + fieldWithPath("a.c").description("two"), + fieldWithPath("a").description("three"))) + .document(operationBuilder("map-request-parts-with-fields") + .request("http://localhost") + .part("part", "{\"a\": {\"b\": 5, \"c\": \"charlie\"}}".getBytes()) + .build()); + } + +}