diff --git a/docs/src/docs/asciidoc/documenting-your-api.adoc b/docs/src/docs/asciidoc/documenting-your-api.adoc
index 7e53f1d9..d1a27442 100644
--- a/docs/src/docs/asciidoc/documenting-your-api.adoc
+++ b/docs/src/docs/asciidoc/documenting-your-api.adoc
@@ -527,6 +527,55 @@ above.
+[[documenting-your-api-request-parts]]
+=== Request parts
+
+The parts of a multipart request can be documenting using `requestParts`. For example:
+
+[source,java,indent=0,role="primary"]
+.MockMvc
+----
+include::{examples-dir}/com/example/mockmvc/RequestParts.java[tags=request-parts]
+----
+<1> Perform a `POST` request with a single part named `file`.
+<2> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses
+ the static `requestParts` method on
+ `org.springframework.restdocs.request.RequestDocumentation`.
+<3> Document the part named `file`. Uses the static `partWithName` method on
+ `org.springframework.restdocs.request.RequestDocumentation`.
+
+[source,java,indent=0,role="secondary"]
+.REST Assured
+----
+include::{examples-dir}/com/example/restassured/RequestParts.java[tags=request-parts]
+----
+<1> Configure Spring REST Docs to produce a snippet describing the request's parts. Uses
+ the static `requestParts` method on
+ `org.springframework.restdocs.request.RequestDocumentation`.
+<2> Document the part named `file`. Uses the static `partWithName` method on
+ `org.springframework.restdocs.request.RequestDocumentation`.
+<3> Configure the request with the part named `file`.
+<4> Perform the `POST` request to `/upload`.
+
+The result is a snippet named `request-parts.adoc` that contains a table describing the
+request parts that are supported by the resource.
+
+When documenting request parts, the test will fail if an undocumented part is used in the
+request. Similarly, the test will also fail if a documented part is not found in the
+request and the part has not been marked as optional.
+
+Request parts can also be documented in a relaxed mode where any undocumented
+parts will not cause a test failure. To do so, use the `relaxedRequestParts` method on
+`org.springframework.restdocs.request.RequestDocumentation`. This can be useful
+when documenting a particular scenario where you only want to focus on a subset of the
+request parts.
+
+If you do not want to document a request part, you can mark it as ignored. This will
+prevent it from appearing in the generated snippet while avoiding the failure described
+above.
+
+
+
[[documenting-your-api-http-headers]]
=== HTTP headers
diff --git a/docs/src/test/java/com/example/mockmvc/RequestParts.java b/docs/src/test/java/com/example/mockmvc/RequestParts.java
new file mode 100644
index 00000000..90210d50
--- /dev/null
+++ b/docs/src/test/java/com/example/mockmvc/RequestParts.java
@@ -0,0 +1,41 @@
+/*
+ * 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.test.web.servlet.MockMvc;
+
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
+import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class RequestParts {
+
+ private MockMvc mockMvc;
+
+ public void upload() throws Exception {
+ // tag::request-parts[]
+ this.mockMvc.perform(fileUpload("/upload").file("file", "example".getBytes())) // <1>
+ .andExpect(status().isOk())
+ .andDo(document("upload", requestParts( // <2>
+ partWithName("file").description("The file to upload")) // <3>
+ ));
+ // end::request-parts[]
+ }
+
+}
diff --git a/docs/src/test/java/com/example/restassured/RequestParts.java b/docs/src/test/java/com/example/restassured/RequestParts.java
new file mode 100644
index 00000000..c6a3f8c1
--- /dev/null
+++ b/docs/src/test/java/com/example/restassured/RequestParts.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.jayway.restassured.RestAssured;
+import com.jayway.restassured.specification.RequestSpecification;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
+import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
+import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document;
+
+public class RequestParts {
+
+ private RequestSpecification spec;
+
+ public void upload() throws Exception {
+ // tag::request-parts[]
+ RestAssured.given(this.spec)
+ .filter(document("users", requestParts( // <1>
+ partWithName("file").description("The file to upload")))) // <2>
+ .multiPart("file", "example") // <3>
+ .when().post("/upload") // <4>
+ .then().statusCode(is(200));
+ // end::request-parts[]
+ }
+
+}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java
index 4c4aee73..4fec764a 100644
--- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestDocumentation.java
@@ -43,6 +43,17 @@ public abstract class RequestDocumentation {
return new ParameterDescriptor(name);
}
+ /**
+ * Creates a {@link RequestPartDescriptor} that describes a request part with the
+ * given {@code name}.
+ *
+ * @param name The name of the request part
+ * @return a {@link RequestPartDescriptor} ready for further configuration
+ */
+ public static RequestPartDescriptor partWithName(String name) {
+ return new RequestPartDescriptor(name);
+ }
+
/**
* Returns a {@code Snippet} that will document the path parameters from the API
* operation's request. The parameters will be documented using the given
@@ -207,4 +218,83 @@ public abstract class RequestDocumentation {
return new RequestParametersSnippet(Arrays.asList(descriptors), attributes, true);
}
+ /**
+ * Returns a {@code Snippet} that will document the parts from the API operation's
+ * request. The parts will be documented using the given {@code descriptors}.
+ *
+ * If a part is present in the request, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part
+ * is documented, is not marked as optional, and is not present in the request, a
+ * failure will also occur.
+ *
+ * If you do not want to document a part, a part descriptor can be marked as
+ * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the
+ * generated snippet while avoiding the failure described above.
+ *
+ * @param descriptors The descriptions of the request's parts
+ * @return the snippet
+ * @see OperationRequest#getParts()
+ */
+ public static RequestPartsSnippet requestParts(RequestPartDescriptor... descriptors) {
+ return new RequestPartsSnippet(Arrays.asList(descriptors));
+ }
+
+ /**
+ * Returns a {@code Snippet} that will document the parts from the API operation's
+ * request. The parameters will be documented using the given {@code descriptors}.
+ *
+ * If a part is documented, is not marked as optional, and is not present in the
+ * request, a failure will occur. Any undocumented parts will be ignored.
+ *
+ * @param descriptors The descriptions of the request's parts
+ * @return the snippet
+ * @see OperationRequest#getParts()
+ */
+ public static RequestPartsSnippet relaxedRequestParts(
+ RequestPartDescriptor... descriptors) {
+ return new RequestPartsSnippet(Arrays.asList(descriptors), true);
+ }
+
+ /**
+ * Returns a {@code Snippet} that will document the parts from the API operation's
+ * request. The given {@code attributes} will be available during snippet rendering
+ * and the parts will be documented using the given {@code descriptors}.
+ *
+ * If a part is present in the request, but is not documented by one of the
+ * descriptors, a failure will occur when the snippet is invoked. Similarly, if a part
+ * is documented, is not marked as optional, and is not present in the request, a
+ * failure will also occur.
+ *
+ * If you do not want to document a part, a part descriptor can be marked as
+ * {@link RequestPartDescriptor#ignored}. This will prevent it from appearing in the
+ * generated snippet while avoiding the failure described above.
+ *
+ * @param attributes the attributes
+ * @param descriptors the descriptions of the request's parts
+ * @return the snippet
+ * @see OperationRequest#getParts()
+ */
+ public static RequestPartsSnippet requestParts(Map attributes,
+ RequestPartDescriptor... descriptors) {
+ return new RequestPartsSnippet(Arrays.asList(descriptors), attributes);
+ }
+
+ /**
+ * Returns a {@code Snippet} that will document the parts from the API operation's
+ * request. The given {@code attributes} will be available during snippet rendering
+ * and the parts will be documented using the given {@code descriptors}.
+ *
+ * If a part is documented, is not marked as optional, and is not present in the
+ * request, a failure will occur. Any undocumented parts will be ignored.
+ *
+ * @param attributes the attributes
+ * @param descriptors the descriptions of the request's parts
+ * @return the snippet
+ * @see OperationRequest#getParameters()
+ */
+ public static RequestPartsSnippet relaxedRequestParts(Map attributes,
+ RequestPartDescriptor... descriptors) {
+ return new RequestPartsSnippet(Arrays.asList(descriptors), attributes, true);
+ }
+
}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java
new file mode 100644
index 00000000..78964960
--- /dev/null
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartDescriptor.java
@@ -0,0 +1,73 @@
+/*
+ * 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.request;
+
+import org.springframework.restdocs.snippet.IgnorableDescriptor;
+
+/**
+ * A descriptor of a request part.
+ *
+ * @author Andy Wilkinson
+ * @see RequestDocumentation#partWithName
+ */
+public class RequestPartDescriptor extends IgnorableDescriptor {
+
+ private final String name;
+
+ private boolean optional;
+
+ /**
+ * Creates a new {@code RequestPartDescriptor} describing the request part with the
+ * given {@code name}.
+ *
+ * @param name the name of the request part
+ */
+ protected RequestPartDescriptor(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Marks the request part as optional.
+ *
+ * @return {@code this}
+ */
+ public final RequestPartDescriptor optional() {
+ this.optional = true;
+ return this;
+ }
+
+ /**
+ * Returns the name of the request part being described by this descriptor.
+ *
+ * @return the name of the parameter
+ */
+ public final String getName() {
+ return this.name;
+ }
+
+ /**
+ * Returns {@code true} if the described request part is optional, otherwise
+ * {@code false}.
+ *
+ * @return {@code true} if the described request part is optional, otherwise
+ * {@code false}
+ */
+ public final boolean isOptional() {
+ return this.optional;
+ }
+
+}
diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java
new file mode 100644
index 00000000..1a5d3059
--- /dev/null
+++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/request/RequestPartsSnippet.java
@@ -0,0 +1,206 @@
+/*
+ * 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.request;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+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.operation.Operation;
+import org.springframework.restdocs.operation.OperationRequestPart;
+import org.springframework.restdocs.snippet.Snippet;
+import org.springframework.restdocs.snippet.SnippetException;
+import org.springframework.restdocs.snippet.TemplatedSnippet;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link Snippet} that documents the request parts supported by a RESTful resource.
+ *
+ * @author Andy Wilkinson
+ * @see RequestDocumentation#requestParts(RequestPartDescriptor...)
+ * @see RequestDocumentation#requestParts(Map, RequestPartDescriptor...)
+ * @see RequestDocumentation#relaxedRequestParts(RequestPartDescriptor...)
+ * @see RequestDocumentation#relaxedRequestParts(Map, RequestPartDescriptor...)
+ */
+public class RequestPartsSnippet extends TemplatedSnippet {
+
+ private final Map descriptorsByName = new LinkedHashMap<>();
+
+ private final boolean ignoreUndocumentedParts;
+
+ /**
+ * Creates a new {@code RequestPartsSnippet} that will document the request's parts
+ * using the given {@code descriptors}. Undocumented parts will trigger a failure.
+ *
+ * @param descriptors the parameter descriptors
+ */
+ protected RequestPartsSnippet(List descriptors) {
+ this(descriptors, null, false);
+ }
+
+ /**
+ * Creates a new {@code RequestPartsSnippet} that will document the request's parts
+ * using the given {@code descriptors}. If {@code ignoreUndocumentedParts} is
+ * {@code true}, undocumented parts will be ignored and will not trigger a failure.
+ *
+ * @param descriptors the parameter descriptors
+ * @param ignoreUndocumentedParts whether undocumented parts should be ignored
+ */
+ protected RequestPartsSnippet(List descriptors,
+ boolean ignoreUndocumentedParts) {
+ this(descriptors, null, ignoreUndocumentedParts);
+ }
+
+ /**
+ * Creates a new {@code RequestPartsSnippet} that will document the request's parts
+ * using the given {@code descriptors}. The given {@code attributes} will be included
+ * in the model during template rendering. Undocumented parts will trigger a failure.
+ *
+ * @param descriptors the parameter descriptors
+ * @param attributes the additional attributes
+ */
+ protected RequestPartsSnippet(List descriptors,
+ Map attributes) {
+ this(descriptors, attributes, false);
+ }
+
+ /**
+ * Creates a new {@code RequestPartsSnippet} that will document the request's parts
+ * using the given {@code descriptors}. The given {@code attributes} will be included
+ * in the model during template rendering. If {@code ignoreUndocumentedParts} is
+ * {@code true}, undocumented parts will be ignored and will not trigger a failure.
+ *
+ * @param descriptors the parameter descriptors
+ * @param attributes the additional attributes
+ * @param ignoreUndocumentedParts whether undocumented parts should be ignored
+ */
+ protected RequestPartsSnippet(List descriptors,
+ Map attributes, boolean ignoreUndocumentedParts) {
+ super("request-parts", attributes);
+ for (RequestPartDescriptor descriptor : descriptors) {
+ Assert.notNull(descriptor.getName(),
+ "Request part descriptors must have a name");
+ if (!descriptor.isIgnored()) {
+ Assert.notNull(descriptor.getDescription(),
+ "The descriptor for request part '" + descriptor.getName()
+ + "' must either have a description or be marked as "
+ + "ignored");
+ }
+ this.descriptorsByName.put(descriptor.getName(), descriptor);
+ }
+ this.ignoreUndocumentedParts = ignoreUndocumentedParts;
+ }
+
+ /**
+ * Returns a new {@code RequestPartsSnippet} 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 RequestPartsSnippet and(RequestPartDescriptor... additionalDescriptors) {
+ List combinedDescriptors = new ArrayList<>();
+ combinedDescriptors.addAll(this.descriptorsByName.values());
+ combinedDescriptors.addAll(Arrays.asList(additionalDescriptors));
+ return new RequestPartsSnippet(combinedDescriptors, this.getAttributes());
+ }
+
+ @Override
+ protected Map createModel(Operation operation) {
+ verifyRequestPartDescriptors(operation);
+ Map model = new HashMap<>();
+ List