Add support for documenting the parts of a multipart request

Closes gh-161
This commit is contained in:
Andy Wilkinson
2016-05-12 15:21:16 +01:00
parent 63a6ad2c91
commit fc3ae3b4a2
20 changed files with 890 additions and 0 deletions

View File

@@ -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

View File

@@ -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[]
}
}

View File

@@ -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[]
}
}

View File

@@ -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}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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}.
* <p>
* 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.
* <p>
* 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<String, Object> 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}.
* <p>
* 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<String, Object> attributes,
RequestPartDescriptor... descriptors) {
return new RequestPartsSnippet(Arrays.asList(descriptors), attributes, true);
}
}

View File

@@ -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<RequestPartDescriptor> {
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;
}
}

View File

@@ -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<String, RequestPartDescriptor> 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<RequestPartDescriptor> 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<RequestPartDescriptor> 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<RequestPartDescriptor> descriptors,
Map<String, Object> 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<RequestPartDescriptor> descriptors,
Map<String, Object> 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<RequestPartDescriptor> combinedDescriptors = new ArrayList<>();
combinedDescriptors.addAll(this.descriptorsByName.values());
combinedDescriptors.addAll(Arrays.asList(additionalDescriptors));
return new RequestPartsSnippet(combinedDescriptors, this.getAttributes());
}
@Override
protected Map<String, Object> createModel(Operation operation) {
verifyRequestPartDescriptors(operation);
Map<String, Object> model = new HashMap<>();
List<Map<String, Object>> requestParts = new ArrayList<>();
for (Entry<String, RequestPartDescriptor> entry : this.descriptorsByName
.entrySet()) {
RequestPartDescriptor descriptor = entry.getValue();
if (!descriptor.isIgnored()) {
requestParts.add(createModelForDescriptor(descriptor));
}
}
model.put("requestParts", requestParts);
return model;
}
private void verifyRequestPartDescriptors(Operation operation) {
Set<String> actualRequestParts = extractActualRequestParts(operation);
Set<String> expectedRequestParts = new HashSet<>();
for (Entry<String, RequestPartDescriptor> entry : this.descriptorsByName
.entrySet()) {
if (!entry.getValue().isOptional()) {
expectedRequestParts.add(entry.getKey());
}
}
Set<String> undocumentedRequestParts;
if (this.ignoreUndocumentedParts) {
undocumentedRequestParts = Collections.emptySet();
}
else {
undocumentedRequestParts = new HashSet<>(actualRequestParts);
undocumentedRequestParts.removeAll(this.descriptorsByName.keySet());
}
Set<String> missingRequestParts = new HashSet<>(expectedRequestParts);
missingRequestParts.removeAll(actualRequestParts);
if (!undocumentedRequestParts.isEmpty() || !missingRequestParts.isEmpty()) {
verificationFailed(undocumentedRequestParts, missingRequestParts);
}
}
private Set<String> extractActualRequestParts(Operation operation) {
Set<String> actualRequestParts = new HashSet<>();
for (OperationRequestPart requestPart : operation.getRequest().getParts()) {
actualRequestParts.add(requestPart.getName());
}
return actualRequestParts;
}
private void verificationFailed(Set<String> undocumentedRequestParts,
Set<String> missingRequestParts) {
String message = "";
if (!undocumentedRequestParts.isEmpty()) {
message += "Request parts with the following names were not documented: "
+ undocumentedRequestParts;
}
if (!missingRequestParts.isEmpty()) {
if (message.length() > 0) {
message += ". ";
}
message += "Request parts with the following names were not found in "
+ "the request: " + missingRequestParts;
}
throw new SnippetException(message);
}
private Map<String, Object> createModelForDescriptor(
RequestPartDescriptor descriptor) {
Map<String, Object> model = new HashMap<>();
model.put("name", descriptor.getName());
model.put("description", descriptor.getDescription());
model.put("optional", descriptor.isOptional());
model.putAll(descriptor.getAttributes());
return model;
}
}

View File

@@ -0,0 +1,9 @@
|===
|Part|Description
{{#requestParts}}
|`{{name}}`
|{{description}}
{{/requestParts}}
|===

View File

@@ -0,0 +1,5 @@
Part | Description
---- | -----------
{{#requestParts}}
`{{name}}` | {{description}}
{{/requestParts}}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2012-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.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.springframework.restdocs.request.RequestDocumentation.partWithName;
/**
* Tests for failures when rendering {@link RequestPartsSnippet} due to missing or
* undocumented request parts.
*
* @author Andy Wilkinson
*/
public class RequestPartsSnippetFailureTests {
@Rule
public ExpectedSnippet snippet = new ExpectedSnippet(TemplateFormats.asciidoctor());
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void undocumentedPart() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo(
"Request parts with the following names were" + " not documented: [a]"));
new RequestPartsSnippet(Collections.<RequestPartDescriptor>emptyList())
.document(new OperationBuilder("undocumented-part",
this.snippet.getOutputDirectory()).request("http://localhost")
.part("a", "alpha".getBytes()).build());
}
@Test
public void missingPart() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Request parts with the following names were"
+ " not found in the request: [a]"));
new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one")))
.document(new OperationBuilder("missing-part",
this.snippet.getOutputDirectory()).request("http://localhost")
.build());
}
@Test
public void undocumentedAndMissingParts() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(equalTo("Request parts with the following names were"
+ " not documented: [b]. Request parts with the following"
+ " names were not found in the request: [a]"));
new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one")))
.document(new OperationBuilder("undocumented-and-missing-parts",
this.snippet.getOutputDirectory()).request("http://localhost")
.part("b", "bravo".getBytes()).build());
}
}

View File

@@ -0,0 +1,183 @@
/*
* 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.io.IOException;
import java.util.Arrays;
import org.junit.Test;
import org.springframework.restdocs.AbstractSnippetTests;
import org.springframework.restdocs.templates.TemplateEngine;
import org.springframework.restdocs.templates.TemplateFormat;
import org.springframework.restdocs.templates.TemplateResourceResolver;
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
/**
* Tests for {@link RequestPartsSnippet}.
*
* @author Andy Wilkinson
*/
public class RequestPartsSnippetTests extends AbstractSnippetTests {
public RequestPartsSnippetTests(String name, TemplateFormat templateFormat) {
super(name, templateFormat);
}
@Test
public void requestParts() throws IOException {
this.snippet.expectRequestParts("request-parts")
.withContents(tableWithHeader("Part", "Description").row("`a`", "one")
.row("`b`", "two"));
new RequestPartsSnippet(Arrays.asList(partWithName("a").description("one"),
partWithName("b").description("two")))
.document(operationBuilder("request-parts")
.request("http://localhost").part("a", "bravo".getBytes())
.and().part("b", "bravo".getBytes()).build());
}
@Test
public void ignoredRequestPart() throws IOException {
this.snippet.expectRequestParts("ignored-request-part")
.withContents(tableWithHeader("Part", "Description").row("`b`", "two"));
new RequestPartsSnippet(Arrays.asList(partWithName("a").ignored(),
partWithName("b").description("two")))
.document(operationBuilder("ignored-request-part")
.request("http://localhost").part("a", "bravo".getBytes())
.and().part("b", "bravo".getBytes()).build());
}
@Test
public void allUndocumentedRequestPartsCanBeIgnored() throws IOException {
this.snippet.expectRequestParts("ignore-all-undocumented")
.withContents(tableWithHeader("Part", "Description").row("`b`", "two"));
new RequestPartsSnippet(Arrays.asList(partWithName("b").description("two")), true)
.document(operationBuilder("ignore-all-undocumented")
.request("http://localhost").part("a", "bravo".getBytes()).and()
.part("b", "bravo".getBytes()).build());
}
@Test
public void missingOptionalRequestPart() throws IOException {
this.snippet.expectRequestParts("missing-optional-request-parts")
.withContents(tableWithHeader("Part", "Description").row("`a`", "one")
.row("`b`", "two"));
new RequestPartsSnippet(
Arrays.asList(partWithName("a").description("one").optional(),
partWithName("b").description("two"))).document(
operationBuilder("missing-optional-request-parts")
.request("http://localhost")
.part("b", "bravo".getBytes()).build());
}
@Test
public void presentOptionalRequestPart() throws IOException {
this.snippet.expectRequestParts("present-optional-request-part")
.withContents(tableWithHeader("Part", "Description").row("`a`", "one"));
new RequestPartsSnippet(
Arrays.asList(partWithName("a").description("one").optional()))
.document(operationBuilder("present-optional-request-part")
.request("http://localhost").part("a", "one".getBytes())
.build());
}
@Test
public void requestPartsWithCustomAttributes() throws IOException {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
given(resolver.resolveTemplateResource("request-parts"))
.willReturn(snippetResource("request-parts-with-title"));
this.snippet.expectRequestParts("request-parts-with-custom-attributes")
.withContents(containsString("The title"));
new RequestPartsSnippet(
Arrays.asList(
partWithName("a").description("one")
.attributes(key("foo").value("alpha")),
partWithName("b").description("two")
.attributes(key("foo").value("bravo"))),
attributes(key("title").value("The title")))
.document(operationBuilder("request-parts-with-custom-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))
.request("http://localhost").part("a", "alpha".getBytes())
.and().part("b", "bravo".getBytes()).build());
}
@Test
public void requestPartsWithCustomDescriptorAttributes() throws IOException {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
given(resolver.resolveTemplateResource("request-parts"))
.willReturn(snippetResource("request-parts-with-extra-column"));
this.snippet.expectRequestParts("request-parts-with-custom-descriptor-attributes")
.withContents(tableWithHeader("Part", "Description", "Foo")
.row("a", "one", "alpha").row("b", "two", "bravo"));
new RequestPartsSnippet(Arrays.asList(
partWithName("a").description("one")
.attributes(key("foo").value("alpha")),
partWithName("b").description("two")
.attributes(key("foo").value("bravo"))))
.document(operationBuilder(
"request-parts-with-custom-descriptor-attributes")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(
resolver))
.request("http://localhost")
.part("a", "alpha".getBytes()).and()
.part("b", "bravo".getBytes()).build());
}
@Test
public void requestPartsWithOptionalColumn() throws IOException {
TemplateResourceResolver resolver = mock(TemplateResourceResolver.class);
given(resolver.resolveTemplateResource("request-parts"))
.willReturn(snippetResource("request-parts-with-optional-column"));
this.snippet.expectRequestParts("request-parts-with-optional-column")
.withContents(tableWithHeader("Part", "Optional", "Description")
.row("a", "true", "one").row("b", "false", "two"));
new RequestPartsSnippet(
Arrays.asList(partWithName("a").description("one").optional(),
partWithName("b").description("two"))).document(
operationBuilder("request-parts-with-optional-column")
.attribute(TemplateEngine.class.getName(),
new MustacheTemplateEngine(resolver))
.request("http://localhost")
.part("a", "alpha".getBytes()).and()
.part("b", "bravo".getBytes()).build());
}
@Test
public void additionalDescriptors() throws IOException {
this.snippet.expectRequestParts("additional-descriptors")
.withContents(tableWithHeader("Part", "Description").row("`a`", "one")
.row("`b`", "two"));
RequestDocumentation.requestParts(partWithName("a").description("one"))
.and(partWithName("b").description("two"))
.document(operationBuilder("additional-descriptors")
.request("http://localhost").part("a", "bravo".getBytes()).and()
.part("b", "bravo".getBytes()).build());
}
}

View File

@@ -126,6 +126,11 @@ public class ExpectedSnippet implements TestRule {
return this;
}
public ExpectedSnippet expectRequestParts(String name) {
expect(name, "request-parts");
return this;
}
private ExpectedSnippet expect(String name, String type) {
this.expectedName = name;
this.expectedType = type;

View File

@@ -0,0 +1,10 @@
|===
|Part|Description|Foo
{{#requestParts}}
|{{name}}
|{{description}}
|{{foo}}
{{/requestParts}}
|===

View File

@@ -0,0 +1,10 @@
|===
|Part|Optional|Description
{{#requestParts}}
|{{name}}
|{{optional}}
|{{description}}
{{/requestParts}}
|===

View File

@@ -0,0 +1,10 @@
.{{title}}
|===
|Part|Description
{{#requestParts}}
|{{name}}
|{{description}}
{{/requestParts}}
|===

View File

@@ -0,0 +1,5 @@
Part | Description | Foo
---- | ----------- | ---
{{#requestParts}}
{{name}} | {{description}} | {{foo}}
{{/requestParts}}

View File

@@ -0,0 +1,5 @@
Part | Optional | Description
---- | -------- | -----------
{{#requestParts}}
{{name}} | {{optional}} | {{description}}
{{/requestParts}}

View File

@@ -0,0 +1,6 @@
{{title}}
Part | Description
---- | -----------
{{#requestParts}}
{{name}} | {{description}}
{{/requestParts}}

View File

@@ -71,8 +71,10 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWit
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.restdocs.templates.TemplateFormats.asciidoctor;
@@ -81,6 +83,7 @@ import static org.springframework.restdocs.test.SnippetMatchers.codeBlock;
import static org.springframework.restdocs.test.SnippetMatchers.httpRequest;
import static org.springframework.restdocs.test.SnippetMatchers.httpResponse;
import static org.springframework.restdocs.test.SnippetMatchers.snippet;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -259,6 +262,20 @@ public class MockMvcRestDocumentationIntegrationTests {
"request-fields.adoc");
}
@Test
public void requestPartsSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation)).build();
mockMvc.perform(fileUpload("/upload").file("foo", "bar".getBytes()))
.andExpect(status().isOk()).andDo(document("request-parts", requestParts(
partWithName("foo").description("The description"))));
assertExpectedSnippetFilesExist(
new File("build/generated-snippets/request-parts"), "http-request.adoc",
"http-response.adoc", "curl-request.adoc", "request-parts.adoc");
}
@Test
public void responseFieldsSnippet() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
@@ -432,6 +449,15 @@ public class MockMvcRestDocumentationIntegrationTests {
"$ curl 'http://localhost:8080/custom/' -i -H 'Accept: application/json'"))));
}
@Test
public void multiPart() throws Exception {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation)).build();
mockMvc.perform(fileUpload("/upload").file("test", "content".getBytes()))
.andExpect(status().isOk()).andDo(document("upload",
requestParts(partWithName("test").description("Foo"))));
}
private void assertExpectedSnippetFilesExist(File directory, String... snippets) {
for (String snippet : snippets) {
assertTrue(new File(directory, snippet).isFile());
@@ -473,6 +499,11 @@ public class MockMvcRestDocumentationIntegrationTests {
return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}";
}
@RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void upload() {
}
}
}

View File

@@ -0,0 +1,9 @@
|===
|Request part|Description
{{#requestParts}}
|`{{name}}`
|{{description}}
{{/requestParts}}
|===

View File

@@ -64,8 +64,10 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWit
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParts;
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document;
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris;
@@ -185,6 +187,17 @@ public class RestAssuredRestDocumentationIntegrationTests {
"http-response.adoc", "curl-request.adoc", "request-fields.adoc");
}
@Test
public void requestPartsSnippet() throws Exception {
given().port(this.port).filter(documentationConfiguration(this.restDocumentation))
.filter(document("request-parts",
requestParts(partWithName("a").description("The description"))))
.multiPart("a", "foo").post("/upload").then().statusCode(200);
assertExpectedSnippetFilesExist(
new File("build/generated-snippets/request-parts"), "http-request.adoc",
"http-response.adoc", "curl-request.adoc", "request-parts.adoc");
}
@Test
public void responseFieldsSnippet() throws Exception {
given().port(this.port).filter(documentationConfiguration(this.restDocumentation))
@@ -343,6 +356,11 @@ public class RestAssuredRestDocumentationIntegrationTests {
return "{\"companyName\": \"FooBar\",\"employee\": [{\"name\": \"Lorem\",\"age\": \"42\"},{\"name\": \"Ipsum\",\"age\": \"24\"}]}";
}
@RequestMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void upload() {
}
}
}