Update field snippets to no longer document whole subsection by default

Previously, when a field was documented it would implicitly document
the whole subsection of the payload identified by that field. This
could lead to users inadvertently failing to document part of the
payload. Arguably, this was a bug as it violated REST Docs' principle
of producing accurate, detail documentation. However, fixing it
requires a breaking change as people may also be relying on this
behaviour. A balance needed to be struck so the fix is being made in
a minor release.

This commit introduces a new subsectionWithPath method which returns a
SubsectionDescriptor; a specialisation of FieldDescriptor. Users
that were intentionally relying on the old behaviour will have to
replace some usage of fieldWithPath with subsectionWithPath instead.
Users who were unintentionally relying on the old behaviour will have
to add some additional descriptors produced using fieldWithPath and
will receive more accurate documentation in return.

Closes gh-274
This commit is contained in:
Andy Wilkinson
2016-10-27 17:33:34 +01:00
parent 7bcfbd9e35
commit cbd96f301d
19 changed files with 830 additions and 252 deletions

View File

@@ -110,7 +110,19 @@ include::{examples-dir}/com/example/Hypermedia.java[tags=ignore-links]
In addition to the hypermedia-specific support <<documenting-your-api-hypermedia,described
above>>, support for general documentation of request and response payloads is also
provided. For example:
provided. Consider the following payload:
[source,json,indent=0]
----
{
"contact": {
"name": "Jane Doe",
"email": "jane.doe@example.com"
}
}
----
It can be documented like this:
[source,java,indent=0,role="primary"]
.MockMvc
@@ -120,9 +132,9 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=response]
<1> Configure Spring REST docs to produce a snippet describing the fields in the response
payload. To document a request `requestFields` can be used. Both are static methods on
`org.springframework.restdocs.payload.PayloadDocumentation`.
<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on
`org.springframework.restdocs.payload.PayloadDocumentation`.
<3> Expect a field with the path `contact.email`.
<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method
on `org.springframework.restdocs.payload.PayloadDocumentation`.
<3> Expect a field with the path `contact.name`.
[source,java,indent=0,role="secondary"]
.REST Assured
@@ -132,9 +144,9 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=response]
<1> Configure Spring REST docs to produce a snippet describing the fields in the response
payload. To document a request `requestFields` can be used. Both are static methods on
`org.springframework.restdocs.payload.PayloadDocumentation`.
<2> Expect a field with the path `contact`. Uses the static `fieldWithPath` method on
<2> Expect a field with the path `contact.email`. Uses the static `fieldWithPath` method on
`org.springframework.restdocs.payload.PayloadDocumentation`.
<3> Expect a field with the path `contact.email`.
<3> Expect a field with the path `contact.name`.
The result is a snippet that contains a table describing the fields. For requests this
snippet is named `request-fields.adoc`. For responses this snippet is named
@@ -142,12 +154,36 @@ snippet is named `request-fields.adoc`. For responses this snippet is named
When documenting fields, the test will fail if an undocumented field is found in the
payload. Similarly, the test will also fail if a documented field is not found in the
payload 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.
payload and the field has not been marked as optional.
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.
If you don't want to provide detailed documentation for all of the fields, an entire
subsection of a payload can be documented. For example:
[source,java,indent=0,role="primary"]
.MockMvc
----
include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection]
----
<1> Document the subsection with the path `contact`. `contact.email` and `contact.name`
are now seen has having also been documented. Uses the static `subsectionWithPath`
method on `org.springframework.restdocs.payload.PayloadDocumentation`.
[source,java,indent=0,role="secondary"]
.REST Assured
----
include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection]
----
<1> Document the subsection with the path `contact`. `contact.email` and `contact.name`
are now seen has having also been documented. Uses the static `subsectionWithPath`
method on `org.springframework.restdocs.payload.PayloadDocumentation`.
`subsectionWithPath` can be useful for providing a high-level overview of a particular
section of a payload. Separate, more detailed documentation for a subsection can then
<<documenting-your-api-request-response-payloads-subsections, be produced>>.
If you do not want to document a field or subsection at all, 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 `relaxedRequestFields` and `relaxedResponseFields`
@@ -233,7 +269,7 @@ The following paths are all present:
|===
A response that uses an array at its root can also be documented. The path `[]` will refer
A payload that uses an array at its root can also be documented. The path `[]` will refer
to the entire array. You can then use bracket or dot notation to identify fields within
the array's entries. For example, `[].id` corresponds to the `id` field of every object
found in the following array:
@@ -403,7 +439,7 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array]
<2> Document `[].title` and `[].author` using the existing descriptors prefixed with `[].`
[[documenting-your-api-request-response-payloads-subsections]]
==== Documenting a portion of a request or response payload
==== Documenting a subsection of a request or response payload
If a payload is large or structurally complex, it can be useful to document
individual sections of the payload. REST Docs allows you to do so by extracting a
@@ -433,7 +469,7 @@ be produced as follows:
[source,java,indent=0,role="primary"]
.MockMvc
----
include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection]
include::{examples-dir}/com/example/mockmvc/Payload.java[tags=beneath-path]
----
<1> Produce a snippet describing the fields in the subsection of the response payload
beneath the path `weather.temperature`. Uses the static `beneathPath` method on
@@ -443,7 +479,7 @@ include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection]
[source,java,indent=0,role="secondary"]
.REST Assured
----
include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection]
include::{examples-dir}/com/example/restassured/Payload.java[tags=beneath-path]
----
<1> Produce a snippet describing the fields in the subsection of the response payload
beneath the path `weather.temperature`. Uses the static `beneathPath` method on
@@ -457,6 +493,7 @@ example, the code above will result in a snippet named
`response-fields-beneath-weather.temperature.adoc`. The identifier can be customized using
the `withSubsectionId(String)` method:
[source,java,indent=0]
----
include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id]
----

View File

@@ -26,6 +26,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuild
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.snippet.Attributes.attributes;
@@ -41,10 +42,19 @@ public class Payload {
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("index", responseFields( // <1>
fieldWithPath("contact").description("The user's contact details"), // <2>
fieldWithPath("contact.email").description("The user's email address")))); // <3>
fieldWithPath("contact.email").description("The user's email address"), // <2>
fieldWithPath("contact.name").description("The user's name")))); // <3>
// end::response[]
}
public void subsection() throws Exception {
// tag::subsection[]
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("index", responseFields( // <1>
subsectionWithPath("contact").description("The user's contact details")))); // <1>
// end::subsection[]
}
public void explicitType() throws Exception {
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
@@ -92,14 +102,14 @@ public class Payload {
// end::book-array[]
}
public void subsection() throws Exception {
// tag::subsection[]
public void subsectionBeneathPath() throws Exception {
// tag::beneath-path[]
this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("location", responseFields(beneathPath("weather.temperature"), // <1>
fieldWithPath("high").description("The forecast high in degrees celcius"), // <2>
fieldWithPath("low").description("The forecast low in degrees celcius"))));
// end::subsection[]
// end::beneath-path[]
}
}

View File

@@ -27,6 +27,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.beneathP
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
@@ -39,12 +40,22 @@ public class Payload {
// tag::response[]
RestAssured.given(this.spec).accept("application/json")
.filter(document("user", responseFields( // <1>
fieldWithPath("contact").description("The user's contact details"), // <2>
fieldWithPath("contact.name").description("The user's name"), // <2>
fieldWithPath("contact.email").description("The user's email address")))) // <3>
.when().get("/user/5")
.then().assertThat().statusCode(is(200));
// end::response[]
}
public void subsection() throws Exception {
// tag::subsection[]
RestAssured.given(this.spec).accept("application/json")
.filter(document("user", responseFields(
subsectionWithPath("contact").description("The user's contact details")))) // <1>
.when().get("/user/5")
.then().assertThat().statusCode(is(200));
// end::response[]
}
public void explicitType() throws Exception {
RestAssured.given(this.spec).accept("application/json")
@@ -96,15 +107,15 @@ public class Payload {
// end::book-array[]
}
public void subsection() throws Exception {
// tag::subsection[]
public void subsectionBeneathPath() throws Exception {
// tag::beneath-path[]
RestAssured.given(this.spec).accept("application/json")
.filter(document("location", responseFields(beneathPath("weather.temperature"), // <1>
fieldWithPath("high").description("The forecast high in degrees celcius"), // <2>
fieldWithPath("low").description("The forecast low in degrees celcius"))))
.when().get("/locations/1")
.then().assertThat().statusCode(is(200));
// end::subsection[]
// end::beneath-path[]
}
}

View File

@@ -26,6 +26,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.pr
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath
import static org.springframework.restdocs.restassured.operation.preprocess.RestAssuredPreprocessors.modifyUris
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration
@@ -75,8 +76,8 @@ class ApiDocumentationSpec extends Specification {
fieldWithPath('appprofile').description('the profile of grails used in this project'),
fieldWithPath('groovyversion').description('the version of groovy used in this project'),
fieldWithPath('jvmversion').description('the version of the jvm used in this project'),
fieldWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'),
fieldWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'),
subsectionWithPath('controllers').type(JsonFieldType.ARRAY).description('the list of available controllers'),
subsectionWithPath('plugins').type(JsonFieldType.ARRAY).description('the plugins active for this project'),
)))
.when()
.port(this.serverPort)
@@ -123,14 +124,14 @@ class ApiDocumentationSpec extends Specification {
requestFields(
fieldWithPath('title').description('the title of the note'),
fieldWithPath('body').description('the body of the note'),
fieldWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note')
subsectionWithPath('tags').type(JsonFieldType.ARRAY).description('a list of tags associated to the note')
),
responseFields(
fieldWithPath('class').description('the class of the resource'),
fieldWithPath('id').description('the id of the note'),
fieldWithPath('title').description('the title of the note'),
fieldWithPath('body').description('the body of the note'),
fieldWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note'),
subsectionWithPath('tags').type(JsonFieldType.ARRAY).description('the list of tags associated with the note')
)))
.body('{ "body": "My test example", "title": "Eureka!", "tags": [{"name": "testing123"}] }')
.when()

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 the original author or authors.
* 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.
@@ -28,6 +28,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuild
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -59,7 +60,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
@SpringApplicationConfiguration(classes = RestNotesSpringDataRest.class)
@WebAppConfiguration
public class ApiDocumentation {
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@@ -116,7 +117,7 @@ public class ApiDocumentation {
linkWithRel("tags").description("The <<resources-tags,Tags resource>>"),
linkWithRel("profile").description("The ALPS profile for the service")),
responseFields(
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources"))));
subsectionWithPath("_links").description("<<resources-index-links,Links>> to other resources"))));
}
@@ -137,8 +138,8 @@ public class ApiDocumentation {
linkWithRel("self").description("Canonical link for this resource"),
linkWithRel("profile").description("The ALPS profile for this resource")),
responseFields(
fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"),
fieldWithPath("_links").description("<<resources-tags-list-links, Links>> to other resources"))));
subsectionWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"),
subsectionWithPath("_links").description("<<resources-tags-list-links, Links>> to other resources"))));
}
@Test
@@ -208,7 +209,7 @@ public class ApiDocumentation {
responseFields(
fieldWithPath("title").description("The title of the note"),
fieldWithPath("body").description("The body of the note"),
fieldWithPath("_links").description("<<resources-note-links,Links>> to other resources"))));
subsectionWithPath("_links").description("<<resources-note-links,Links>> to other resources"))));
}
@Test
@@ -227,8 +228,8 @@ public class ApiDocumentation {
linkWithRel("self").description("Canonical link for this resource"),
linkWithRel("profile").description("The ALPS profile for this resource")),
responseFields(
fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"),
fieldWithPath("_links").description("<<resources-tags-list-links, Links>> to other resources"))));
subsectionWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"),
subsectionWithPath("_links").description("<<resources-tags-list-links, Links>> to other resources"))));
}
@Test
@@ -310,7 +311,7 @@ public class ApiDocumentation {
linkWithRel("notes").description("The <<resources-tagged-notes,notes>> that have this tag")),
responseFields(
fieldWithPath("name").description("The name of the tag"),
fieldWithPath("_links").description("<<resources-tag-links,Links>> to other resources"))));
subsectionWithPath("_links").description("<<resources-tag-links,Links>> to other resources"))));
}
@Test

View File

@@ -33,6 +33,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.pr
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -68,11 +69,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
@SpringApplicationConfiguration(classes = RestNotesSpringHateoas.class)
@WebAppConfiguration
public class ApiDocumentation {
@Rule
public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
private RestDocumentationResultHandler documentationHandler;
private RestDocumentationResultHandler documentationHandler;
@Autowired
private NoteRepository noteRepository;
@@ -93,13 +94,13 @@ public class ApiDocumentation {
this.documentationHandler = document("{method-name}",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()));
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation))
.alwaysDo(this.documentationHandler)
.build();
}
@Test
public void headersExample() throws Exception {
this.mockMvc
@@ -140,7 +141,7 @@ public class ApiDocumentation {
linkWithRel("notes").description("The <<resources-notes,Notes resource>>"),
linkWithRel("tags").description("The <<resources-tags,Tags resource>>")),
responseFields(
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources"))));
subsectionWithPath("_links").description("<<resources-index-links,Links>> to other resources"))));
}
@Test
@@ -150,13 +151,13 @@ public class ApiDocumentation {
createNote("REST maturity model", "http://martinfowler.com/articles/richardsonMaturityModel.html");
createNote("Hypertext Application Language (HAL)", "http://stateless.co/hal_specification.html");
createNote("Application-Level Profile Semantics (ALPS)", "http://alps.io/spec/");
this.mockMvc
.perform(get("/notes"))
.andExpect(status().isOk())
.andDo(this.documentationHandler.document(
responseFields(
fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"))));
subsectionWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>"))));
}
@Test
@@ -177,7 +178,7 @@ public class ApiDocumentation {
note.put("tags", Arrays.asList(tagLocation));
ConstrainedFields fields = new ConstrainedFields(NoteInput.class);
this.mockMvc
.perform(post("/notes")
.contentType(MediaTypes.HAL_JSON)
@@ -214,7 +215,7 @@ public class ApiDocumentation {
.content(this.objectMapper.writeValueAsString(note)))
.andExpect(status().isCreated())
.andReturn().getResponse().getHeader("Location");
this.mockMvc
.perform(get(noteLocation))
.andExpect(status().isOk())
@@ -229,7 +230,7 @@ public class ApiDocumentation {
responseFields(
fieldWithPath("title").description("The title of the note"),
fieldWithPath("body").description("The body of the note"),
fieldWithPath("_links").description("<<resources-note-links,Links>> to other resources"))));
subsectionWithPath("_links").description("<<resources-note-links,Links>> to other resources"))));
}
@@ -241,13 +242,13 @@ public class ApiDocumentation {
createTag("REST");
createTag("Hypermedia");
createTag("HTTP");
this.mockMvc
.perform(get("/tags"))
.andExpect(status().isOk())
.andDo(this.documentationHandler.document(
responseFields(
fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"))));
subsectionWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>"))));
}
@Test
@@ -256,7 +257,7 @@ public class ApiDocumentation {
tag.put("name", "REST");
ConstrainedFields fields = new ConstrainedFields(TagInput.class);
this.mockMvc
.perform(post("/tags")
.contentType(MediaTypes.HAL_JSON)
@@ -344,7 +345,7 @@ public class ApiDocumentation {
linkWithRel("tagged-notes").description("The <<resources-tagged-notes,notes>> that have this tag")),
responseFields(
fieldWithPath("name").description("The name of the tag"),
fieldWithPath("_links").description("<<resources-tag-links,Links>> to other resources"))));
subsectionWithPath("_links").description("<<resources-tag-links,Links>> to other resources"))));
}
@Test
@@ -363,7 +364,7 @@ public class ApiDocumentation {
tagUpdate.put("name", "RESTful");
ConstrainedFields fields = new ConstrainedFields(TagPatchInput.class);
this.mockMvc
.perform(patch(tagLocation)
.contentType(MediaTypes.HAL_JSON)

View File

@@ -155,12 +155,10 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {
for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath(), "Field descriptors must have a path");
if (!descriptor.isIgnored()) {
Assert.notNull(descriptor.getDescription(),
"The descriptor for field '" + descriptor.getPath()
+ "' must either have a description or" + " be marked as "
+ "ignored");
Assert.notNull(descriptor.getDescription() != null,
"The descriptor for '" + descriptor.getPath() + "' must have a"
+ " description or it must be marked as ignored");
}
}
this.fieldDescriptors = descriptors;
this.ignoreUndocumentedFields = ignoreUndocumentedFields;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 the original author or authors.
* 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.

View File

@@ -65,7 +65,12 @@ class JsonContentHandler implements ContentHandler {
Object content = readContent();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
JsonFieldPath path = JsonFieldPath.compile(fieldDescriptor.getPath());
this.fieldProcessor.remove(path, content);
if (describesSubsection(fieldDescriptor)) {
this.fieldProcessor.removeSubsection(path, content);
}
else {
this.fieldProcessor.remove(path, content);
}
}
if (!isEmpty(content)) {
try {
@@ -78,6 +83,10 @@ class JsonContentHandler implements ContentHandler {
return null;
}
private boolean describesSubsection(FieldDescriptor fieldDescriptor) {
return fieldDescriptor instanceof SubsectionDescriptor;
}
private Object readContent() {
try {
return new ObjectMapper().readValue(this.rawContent, Object.class);

View File

@@ -76,6 +76,17 @@ final class JsonFieldProcessor {
});
}
void removeSubsection(final JsonFieldPath path, Object payload) {
traverse(new ProcessingContext(payload, path), new MatchCallback() {
@Override
public void foundMatch(Match match) {
match.removeSubsection();
}
});
}
private void traverse(ProcessingContext context, MatchCallback matchCallback) {
final String segment = context.getSegment();
if (JsonFieldPath.isArraySegment(segment)) {
@@ -149,12 +160,41 @@ final class JsonFieldProcessor {
@Override
public void remove() {
Object removalCandidate = this.map.get(this.segment);
if (isMapWithEntries(removalCandidate)
|| isListWithNonScalarEntries(removalCandidate)) {
return;
}
this.map.remove(this.segment);
if (this.map.isEmpty() && this.parent != null) {
this.parent.remove();
}
}
@Override
public void removeSubsection() {
this.map.remove(this.segment);
if (this.map.isEmpty() && this.parent != null) {
this.parent.removeSubsection();
}
}
private boolean isMapWithEntries(Object object) {
return object instanceof Map && !((Map<?, ?>) object).isEmpty();
}
private boolean isListWithNonScalarEntries(Object object) {
if (!(object instanceof List)) {
return false;
}
for (Object entry : (List<?>) object) {
if (entry instanceof Map || entry instanceof List) {
return true;
}
}
return false;
}
}
private static final class ListMatch implements Match {
@@ -181,12 +221,35 @@ final class JsonFieldProcessor {
@Override
public void remove() {
if (!itemIsEmpty()) {
return;
}
this.items.remove();
if (this.list.isEmpty() && this.parent != null) {
this.parent.remove();
}
}
@Override
public void removeSubsection() {
this.items.remove();
if (this.list.isEmpty() && this.parent != null) {
this.parent.removeSubsection();
}
}
private boolean itemIsEmpty() {
return !isMapWithEntries(this.item) && !isListWithEntries(this.item);
}
private boolean isMapWithEntries(Object object) {
return object instanceof Map && !((Map<?, ?>) object).isEmpty();
}
private boolean isListWithEntries(Object object) {
return object instanceof List && !((List<?>) object).isEmpty();
}
}
private interface MatchCallback {
@@ -200,6 +263,8 @@ final class JsonFieldProcessor {
Object getValue();
void remove();
void removeSubsection();
}
private static final class ProcessingContext {

View File

@@ -102,6 +102,74 @@ public abstract class PayloadDocumentation {
return new FieldDescriptor(path);
}
/**
* Creates a {@code FieldDescriptor} that describes a subsection, i.e. a field and all
* of its descendants, with the given {@code path}.
* <p>
* When documenting an XML payload, the {@code path} uses XPath, i.e. '/' is used to
* descend to a child node.
* <p>
* When documenting a JSON payload, the {@code path} uses '.' to descend into a child
* object and ' {@code []}' to descend into an array. For example, with this JSON
* payload:
*
* <pre>
* {
* "a":{
* "b":[
* {
* "c":"one"
* },
* {
* "c":"two"
* },
* {
* "d":"three"
* }
* ]
* }
* }
* </pre>
*
* The following paths are all present:
*
* <table summary="Paths and their values">
* <tr>
* <th>Path</th>
* <th>Value</th>
* </tr>
* <tr>
* <td>{@code a}</td>
* <td>An object containing "b"</td>
* </tr>
* <tr>
* <td>{@code a.b}</td>
* <td>An array containing three objects</td>
* </tr>
* <tr>
* <td>{@code a.b[]}</td>
* <td>An array containing three objects</td>
* </tr>
* <tr>
* <td>{@code a.b[].c}</td>
* <td>An array containing the strings "one" and "two"</td>
* </tr>
* <tr>
* <td>{@code a.b[].d}</td>
* <td>The string "three"</td>
* </tr>
* </table>
* <p>
* A subsection descriptor for the array with the path {@code a.b[]} will also
* describe its descendants {@code a.b[].c} and {@code a.b[].d}.
*
* @param path The path of the subsection
* @return a {@code SubsectionDescriptor} ready for further configuration
*/
public static SubsectionDescriptor subsectionWithPath(String path) {
return new SubsectionDescriptor(path);
}
/**
* 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}.
@@ -110,16 +178,19 @@ public abstract class PayloadDocumentation {
* 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.
* a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean
* that all of its descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see FieldDescriptor#description(Object)
*/
public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) {
return requestFields(Arrays.asList(descriptors));
@@ -133,16 +204,18 @@ public abstract class PayloadDocumentation {
* 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.
* a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean
* that all of its descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestFieldsSnippet requestFields(List<FieldDescriptor> descriptors) {
return new RequestFieldsSnippet(descriptors);
@@ -158,6 +231,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestFieldsSnippet relaxedRequestFields(
FieldDescriptor... descriptors) {
@@ -174,6 +248,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestFieldsSnippet relaxedRequestFields(
List<FieldDescriptor> descriptors) {
@@ -187,19 +262,21 @@ public abstract class PayloadDocumentation {
* <p>
* 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
* payload, 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.
* 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 with a {@link #subsectionWithPath(String) subsection descriptor} will mean
* that all of its descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param attributes the attributes
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestFieldsSnippet requestFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
@@ -213,19 +290,21 @@ public abstract class PayloadDocumentation {
* <p>
* 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
* payload, 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.
* 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 with a {@link #subsectionWithPath(String) subsection descriptor} will mean
* that all of its descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param attributes the attributes
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestFieldsSnippet requestFields(Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
@@ -244,6 +323,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestFieldsSnippet relaxedRequestFields(
Map<String, Object> attributes, FieldDescriptor... descriptors) {
@@ -262,6 +342,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestFieldsSnippet relaxedRequestFields(
Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
@@ -273,22 +354,25 @@ public abstract class PayloadDocumentation {
* operations's request payload extracted by the given {@code subsectionExtractor}.
* The fields will be documented using the given {@code descriptors}.
* <p>
* 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 a field is present in the subsection of 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 subsection, a failure will also occur.For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param subsectionExtractor the subsection extractor
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestFieldsSnippet requestFields(
@@ -302,22 +386,25 @@ public abstract class PayloadDocumentation {
* API operations's request payload extracted by the given {@code subsectionExtractor}
* . The fields will be documented using the given {@code descriptors}.
* <p>
* 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 a field is present in the subsection of 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 subsection, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param subsectionExtractor the subsection extractor
* @param descriptors the descriptions of the request payload's fields
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestFieldsSnippet requestFields(
@@ -339,6 +426,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestFieldsSnippet relaxedRequestFields(
@@ -360,6 +448,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestFieldsSnippet relaxedRequestFields(
@@ -374,16 +463,18 @@ public abstract class PayloadDocumentation {
* The fields will be documented using the given {@code descriptors} and the given
* {@code attributes} will be available during snippet generation.
* <p>
* 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
* payload, 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 a field is present in the subsection of 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 subsection, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param subsectionExtractor the subsection extractor
* @param attributes the attributes
@@ -391,6 +482,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestFieldsSnippet requestFields(
@@ -405,16 +497,18 @@ public abstract class PayloadDocumentation {
* The fields will be documented using the given {@code descriptors} and the given
* {@code attributes} will be available during snippet generation.
* <p>
* 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
* payload, 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 a field is present in the subsection of 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 subsection, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param subsectionExtractor the subsection extractor
* @param attributes the attributes
@@ -422,6 +516,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestFieldsSnippet requestFields(
@@ -445,6 +540,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestFieldsSnippet relaxedRequestFields(
@@ -469,6 +565,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestFieldsSnippet relaxedRequestFields(
@@ -483,22 +580,25 @@ public abstract class PayloadDocumentation {
* {@code part} of the API operations's request payload. The fields will be documented
* using the given {@code descriptors}.
* <p>
* 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 a field is present in the payload of 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's payload, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param part the part name
* @param descriptors the descriptions of the request part's fields
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestPartFieldsSnippet requestPartFields(String part,
FieldDescriptor... descriptors) {
@@ -510,21 +610,24 @@ public abstract class PayloadDocumentation {
* {@code part} of the API operations's request payload. The fields will be documented
* using the given {@code descriptors}.
* <p>
* 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 a field is present in the payload of 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's payload, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @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)
* @see #subsectionWithPath(String)
*/
public static RequestPartFieldsSnippet requestPartFields(String part,
List<FieldDescriptor> descriptors) {
@@ -543,6 +646,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the request part's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestPartFieldsSnippet relaxedRequestPartFields(String part,
FieldDescriptor... descriptors) {
@@ -561,6 +665,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the request part's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestPartFieldsSnippet relaxedRequestPartFields(String part,
List<FieldDescriptor> descriptors) {
@@ -573,22 +678,25 @@ public abstract class PayloadDocumentation {
* using the given {@code descriptors} and the given {@code attributes} will be
* available during snippet generation.
* <p>
* 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 a field is present in the payload of 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's payload, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @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)
* @see #subsectionWithPath(String)
*/
public static RequestPartFieldsSnippet requestPartFields(String part,
Map<String, Object> attributes, FieldDescriptor... descriptors) {
@@ -601,22 +709,25 @@ public abstract class PayloadDocumentation {
* using the given {@code descriptors} and the given {@code attributes} will be
* available during snippet generation.
* <p>
* 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 a field is present in the payload of 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's payload, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @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)
* @see #subsectionWithPath(String)
*/
public static RequestPartFieldsSnippet requestPartFields(String part,
Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
@@ -637,6 +748,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the request part's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestPartFieldsSnippet relaxedRequestPartFields(String part,
Map<String, Object> attributes, FieldDescriptor... descriptors) {
@@ -657,6 +769,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the request part's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static RequestPartFieldsSnippet relaxedRequestPartFields(String part,
Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
@@ -669,16 +782,18 @@ public abstract class PayloadDocumentation {
* be extracted by the given {@code subsectionExtractor}. The fields will be
* documented using the given {@code descriptors}.
* <p>
* 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 a field is present in the subsection of the request part 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 subsection, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param part the part name
* @param subsectionExtractor the subsection extractor
@@ -686,6 +801,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestPartFieldsSnippet requestPartFields(String part,
@@ -700,16 +816,18 @@ public abstract class PayloadDocumentation {
* be extracted by the given {@code subsectionExtractor}. The fields will be
* documented using the given {@code descriptors}.
* <p>
* 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 a field is present in the subsection of the request part 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 subsection, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param part the part name
* @param subsectionExtractor the subsection extractor
@@ -717,6 +835,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestPartFieldsSnippet requestPartFields(String part,
@@ -740,6 +859,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestPartFieldsSnippet relaxedRequestPartFields(String part,
@@ -764,6 +884,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestPartFieldsSnippet relaxedRequestPartFields(String part,
@@ -779,16 +900,18 @@ public abstract class PayloadDocumentation {
* documented using the given {@code descriptors} and the given {@code attributes}
* will be available during snippet generation.
* <p>
* 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 a field is present in the subsection of the request part 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 subsection, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param part the part name
* @param subsectionExtractor the subsection extractor
@@ -797,6 +920,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestPartFieldsSnippet requestPartFields(String part,
@@ -813,16 +937,18 @@ public abstract class PayloadDocumentation {
* documented using the given {@code descriptors} and the given {@code attributes}
* will be available during snippet generation.
* <p>
* 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 a field is present in the subsection of the request part 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 subsection, a failure will also occur. For payloads with a
* hierarchical structure, documenting a field with a
* {@link #subsectionWithPath(String) subsection descriptor} will mean that all of its
* descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param part the part name
* @param subsectionExtractor the subsection extractor
@@ -831,6 +957,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestPartFieldsSnippet requestPartFields(String part,
@@ -857,6 +984,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestPartFieldsSnippet relaxedRequestPartFields(String part,
@@ -883,6 +1011,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static RequestPartFieldsSnippet relaxedRequestPartFields(String part,
@@ -899,18 +1028,20 @@ public abstract class PayloadDocumentation {
* <p>
* If a field is present in the response 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 response
* payload, 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.
* field is documented, is not marked as optional, and is not present in the response,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean
* that all of its descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) {
return responseFields(Arrays.asList(descriptors));
@@ -923,19 +1054,21 @@ public abstract class PayloadDocumentation {
* <p>
* If a field is present in the response 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 response
* payload, 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.
* field is documented, is not marked as optional, and is not present in the response,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean
* that all of its descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet responseFields(
@@ -955,6 +1088,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet relaxedResponseFields(
@@ -973,6 +1107,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static ResponseFieldsSnippet relaxedResponseFields(
List<FieldDescriptor> descriptors) {
@@ -986,19 +1121,21 @@ public abstract class PayloadDocumentation {
* <p>
* If a field is present in the response 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 response
* payload, 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.
* field is documented, is not marked as optional, and is not present in the response,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean
* that all of its descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param attributes the attributes
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static ResponseFieldsSnippet responseFields(Map<String, Object> attributes,
FieldDescriptor... descriptors) {
@@ -1012,19 +1149,21 @@ public abstract class PayloadDocumentation {
* <p>
* If a field is present in the response 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 response
* payload, 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.
* field is documented, is not marked as optional, and is not present in the response,
* a failure will also occur. For payloads with a hierarchical structure, documenting
* a field with a {@link #subsectionWithPath(String) subsection descriptor} will mean
* that all of its descendants are also treated as having been documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param attributes the attributes
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static ResponseFieldsSnippet responseFields(Map<String, Object> attributes,
List<FieldDescriptor> descriptors) {
@@ -1043,6 +1182,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static ResponseFieldsSnippet relaxedResponseFields(
Map<String, Object> attributes, FieldDescriptor... descriptors) {
@@ -1061,6 +1201,7 @@ public abstract class PayloadDocumentation {
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
*/
public static ResponseFieldsSnippet relaxedResponseFields(
Map<String, Object> attributes, List<FieldDescriptor> descriptors) {
@@ -1077,18 +1218,21 @@ public abstract class PayloadDocumentation {
* 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 response
* payload, 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.
* documenting a field with a {@link #subsectionWithPath(String) subsection
* descriptor} will mean that all of its descendants are also treated as having been
* documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param subsectionExtractor the subsection extractor
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet responseFields(
@@ -1107,18 +1251,21 @@ public abstract class PayloadDocumentation {
* 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 response
* payload, 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.
* documenting a field with a {@link #subsectionWithPath(String) subsection
* descriptor} will mean that all of its descendants are also treated as having been
* documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param subsectionExtractor the subsection extractor
* @param descriptors the descriptions of the response payload's fields
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet responseFields(
@@ -1141,6 +1288,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet relaxedResponseFields(
@@ -1163,6 +1311,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet relaxedResponseFields(
@@ -1182,12 +1331,14 @@ public abstract class PayloadDocumentation {
* 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 response
* payload, 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.
* documenting a field with a {@link #subsectionWithPath(String) subsection
* descriptor} will mean that all of its descendants are also treated as having been
* documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param subsectionExtractor the subsection extractor
* @param attributes the attributes
@@ -1195,6 +1346,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet responseFields(
@@ -1215,12 +1367,14 @@ public abstract class PayloadDocumentation {
* 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 response
* payload, 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.
* documenting a field with a {@link #subsectionWithPath(String) subsection
* descriptor} will mean that all of its descendants are also treated as having been
* documented.
* <p>
* 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.
* If you do not want to document a field or subsection, a descriptor can be
* {@link FieldDescriptor#ignored configured to ignore it}. The ignored field or
* subsection will not appear in the generated snippet and the failure described above
* will not occur.
*
* @param subsectionExtractor the subsection extractor
* @param attributes the attributes
@@ -1228,6 +1382,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet responseFields(
@@ -1252,6 +1407,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet relaxedResponseFields(
@@ -1277,6 +1433,7 @@ public abstract class PayloadDocumentation {
* @return the snippet that will document the fields
* @since 1.2.0
* @see #fieldWithPath(String)
* @see #subsectionWithPath(String)
* @see #beneathPath(String)
*/
public static ResponseFieldsSnippet relaxedResponseFields(
@@ -1298,11 +1455,13 @@ public abstract class PayloadDocumentation {
List<FieldDescriptor> descriptors) {
List<FieldDescriptor> prefixedDescriptors = new ArrayList<>();
for (FieldDescriptor descriptor : descriptors) {
FieldDescriptor prefixedDescriptor = new FieldDescriptor(
pathPrefix + descriptor.getPath())
.description(descriptor.getDescription())
.type(descriptor.getType())
.attributes(asArray(descriptor.getAttributes()));
String prefixedPath = pathPrefix + descriptor.getPath();
FieldDescriptor prefixedDescriptor = descriptor instanceof SubsectionDescriptor
? new SubsectionDescriptor(prefixedPath)
: new FieldDescriptor(prefixedPath);
prefixedDescriptor.description(descriptor.getDescription())
.type(descriptor.getType())
.attributes(asArray(descriptor.getAttributes()));
if (descriptor.isIgnored()) {
prefixedDescriptor.ignored();
}

View File

@@ -0,0 +1,37 @@
/*
* 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;
/**
* A description of a subsection, i.e. a field and all of its descendants, in a request or
* response payload.
*
* @author Andy Wilkinson
* @since 1.2.0
*/
public class SubsectionDescriptor extends FieldDescriptor {
/**
* Creates a new {@code SubsectionDescriptor} describing the subsection with the given
* {@code path}.
* @param path the path
*/
protected SubsectionDescriptor(String path) {
super(path);
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.restdocs.payload;
import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
@@ -36,6 +37,7 @@ import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
@@ -108,6 +110,7 @@ class XmlContentHandler implements ContentHandler {
@Override
public String getUndocumentedContent(List<FieldDescriptor> fieldDescriptors) {
Document payload = readPayload();
List<Node> matchedButNotRemoved = new ArrayList<>();
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
NodeList matchingNodes;
try {
@@ -124,17 +127,50 @@ class XmlContentHandler implements ContentHandler {
attr.getOwnerElement().removeAttributeNode(attr);
}
else {
node.getParentNode().removeChild(node);
if (fieldDescriptor instanceof SubsectionDescriptor
|| isLeafNode(node)) {
node.getParentNode().removeChild(node);
}
else {
matchedButNotRemoved.add(node);
}
}
}
}
removeLeafNodes(matchedButNotRemoved);
if (payload.getChildNodes().getLength() > 0) {
return prettyPrint(payload);
}
return null;
}
private void removeLeafNodes(List<Node> candidates) {
boolean changed = true;
while (changed) {
changed = false;
Iterator<Node> iterator = candidates.iterator();
while (iterator.hasNext()) {
Node node = iterator.next();
if (isLeafNode(node)) {
node.getParentNode().removeChild(node);
iterator.remove();
changed = true;
}
}
}
}
private boolean isLeafNode(Node node) {
NodeList childNodes = node.getChildNodes();
for (int i = 0; i < childNodes.getLength(); i++) {
if (childNodes.item(i) instanceof Element) {
return false;
}
}
return true;
}
private String prettyPrint(Document document) {
try {
StringWriter stringWriter = new StringWriter();

View File

@@ -201,6 +201,26 @@ public class JsonFieldProcessorTests {
assertThat(payload.size(), equalTo(0));
}
@Test
public void mapWithEntriesIsNotRemovedWhenNotAlsoRemovingDescendants() {
Map<String, Object> payload = new HashMap<>();
Map<String, Object> alpha = new HashMap<>();
payload.put("a", alpha);
alpha.put("b", "bravo");
this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload);
assertThat(payload.size(), equalTo(1));
}
@Test
public void removeSubsectionRemovesMapWithEntries() {
Map<String, Object> payload = new HashMap<>();
Map<String, Object> alpha = new HashMap<>();
payload.put("a", alpha);
alpha.put("b", "bravo");
this.fieldProcessor.removeSubsection(JsonFieldPath.compile("a"), payload);
assertThat(payload.size(), equalTo(0));
}
@Test
public void removeNestedMapEntry() {
Map<String, Object> payload = new HashMap<>();
@@ -229,6 +249,51 @@ public class JsonFieldProcessorTests {
assertThat(payload.size(), equalTo(0));
}
@SuppressWarnings("unchecked")
@Test
public void removeDoesNotRemoveArrayWithMapEntries() throws IOException {
Map<String, Object> payload = new ObjectMapper()
.readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class);
this.fieldProcessor.remove(JsonFieldPath.compile("a[]"), payload);
assertThat(payload.size(), equalTo(1));
}
@SuppressWarnings("unchecked")
@Test
public void removeDoesNotRemoveArrayWithListEntries() throws IOException {
Map<String, Object> payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}",
Map.class);
this.fieldProcessor.remove(JsonFieldPath.compile("a[]"), payload);
assertThat(payload.size(), equalTo(1));
}
@SuppressWarnings("unchecked")
@Test
public void removeRemovesArrayWithOnlyScalarEntries() throws IOException {
Map<String, Object> payload = new ObjectMapper()
.readValue("{\"a\": [\"bravo\", \"charlie\"]}", Map.class);
this.fieldProcessor.remove(JsonFieldPath.compile("a"), payload);
assertThat(payload.size(), equalTo(0));
}
@SuppressWarnings("unchecked")
@Test
public void removeSubsectionRemovesArrayWithMapEntries() throws IOException {
Map<String, Object> payload = new ObjectMapper()
.readValue("{\"a\": [{\"b\":\"bravo\"},{\"b\":\"bravo\"}]}", Map.class);
this.fieldProcessor.removeSubsection(JsonFieldPath.compile("a[]"), payload);
assertThat(payload.size(), equalTo(0));
}
@SuppressWarnings("unchecked")
@Test
public void removeSubsectionRemovesArrayWithListEntries() throws IOException {
Map<String, Object> payload = new ObjectMapper().readValue("{\"a\": [[2],[3]]}",
Map.class);
this.fieldProcessor.removeSubsection(JsonFieldPath.compile("a[]"), payload);
assertThat(payload.size(), equalTo(0));
}
@Test
public void extractNestedEntryWithDotInKeys() throws IOException {
Map<String, Object> payload = new HashMap<>();

View File

@@ -131,8 +131,8 @@ public class RequestFieldsSnippetFailureTests {
@Test
public void undocumentedXmlRequestField() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(startsWith(
"The following parts of the payload were not" + " documented:"));
this.thrown.expectMessage(
startsWith("The following parts of the payload were not documented:"));
new RequestFieldsSnippet(Collections.<FieldDescriptor>emptyList())
.document(this.operationBuilder.request("http://localhost")
.content("<a><b>5</b></a>").header(HttpHeaders.CONTENT_TYPE,
@@ -140,6 +140,20 @@ public class RequestFieldsSnippetFailureTests {
.build());
}
@Test
public void xmlDescendentsAreNotDocumentedByFieldDescriptor() throws IOException {
this.thrown.expect(SnippetException.class);
this.thrown.expectMessage(
startsWith("The following parts of the payload were not documented:"));
new RequestFieldsSnippet(
Arrays.asList(fieldWithPath("a").type("a").description("one")))
.document(this.operationBuilder.request("http://localhost")
.content("<a><b>5</b></a>")
.header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
public void xmlRequestFieldWithNoType() throws IOException {
this.thrown.expect(FieldTypeRequiredException.class);

View File

@@ -36,6 +36,7 @@ import static org.mockito.Mockito.mock;
import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
@@ -65,6 +66,19 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests {
.build());
}
@Test
public void entireSubsectionsCanBeDocumented() throws IOException {
this.snippets.expectRequestFields()
.withContents(tableWithHeader("Path", "Type", "Description").row("`a`",
"`Object`", "one"));
new RequestFieldsSnippet(
Arrays.asList(subsectionWithPath("a").description("one")))
.document(this.operationBuilder.request("http://localhost")
.content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}")
.build());
}
@Test
public void subsectionOfMapRequest() throws IOException {
this.snippets.expect("request-fields-beneath-a")
@@ -121,6 +135,18 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests {
.content("{\"a\": 5, \"b\": 4}").build());
}
@Test
public void entireSubsectionCanBeIgnored() throws IOException {
this.snippets.expectRequestFields()
.withContents(tableWithHeader("Path", "Type", "Description").row("`c`",
"`Number`", "Field c"));
new RequestFieldsSnippet(Arrays.asList(subsectionWithPath("a").ignored(),
fieldWithPath("c").description("Field c")))
.document(this.operationBuilder.request("http://localhost")
.content("{\"a\": {\"b\": 5}, \"c\": 4}").build());
}
@Test
public void allUndocumentedRequestFieldsCanBeIgnored() throws IOException {
this.snippets.expectRequestFields()
@@ -256,6 +282,20 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests {
.build());
}
@Test
public void entireSubsectionOfXmlPayloadCanBeDocumented() throws IOException {
this.snippets.expectRequestFields().withContents(
tableWithHeader("Path", "Type", "Description").row("`a`", "`a`", "one"));
new RequestFieldsSnippet(
Arrays.asList(subsectionWithPath("a").description("one").type("a")))
.document(this.operationBuilder.request("http://localhost")
.content("<a><b>5</b><c>charlie</c></a>")
.header(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_XML_VALUE)
.build());
}
@Test
public void additionalDescriptors() throws IOException {
this.snippets.expectRequestFields()

View File

@@ -0,0 +1,91 @@
/*
* 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.util.Arrays;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
/**
* Tests for {@link XmlContentHandler}.
*
* @author Andy Wilkinson
*/
public class XmlContentHandlerTests {
@Test
public void topLevelElementCanBeDocumented() {
String undocumentedContent = createHandler("<a>5</a>").getUndocumentedContent(
Arrays.asList(fieldWithPath("a").type("a").description("description")));
assertThat(undocumentedContent, is(nullValue()));
}
@Test
public void nestedElementCanBeDocumentedLeavingAncestors() {
String undocumentedContent = createHandler("<a><b>5</b></a>")
.getUndocumentedContent(Arrays.asList(
fieldWithPath("a/b").type("b").description("description")));
assertThat(undocumentedContent, is(equalTo(String.format("<a/>%n"))));
}
@Test
public void fieldDescriptorDoesNotDocumentEntireSubsection() {
String undocumentedContent = createHandler("<a><b>5</b></a>")
.getUndocumentedContent(Arrays
.asList(fieldWithPath("a").type("a").description("description")));
assertThat(undocumentedContent,
is(equalTo(String.format("<a>%n <b>5</b>%n</a>%n"))));
}
@Test
public void subsectionDescriptorDocumentsEntireSubsection() {
String undocumentedContent = createHandler("<a><b>5</b></a>")
.getUndocumentedContent(Arrays.asList(
subsectionWithPath("a").type("a").description("description")));
assertThat(undocumentedContent, is(nullValue()));
}
@Test
public void multipleElementsCanBeInDescendingOrderDocumented() {
String undocumentedContent = createHandler("<a><b>5</b></a>")
.getUndocumentedContent(Arrays.asList(
fieldWithPath("a").type("a").description("description"),
fieldWithPath("a/b").type("b").description("description")));
assertThat(undocumentedContent, is(nullValue()));
}
@Test
public void multipleElementsCanBeInAscendingOrderDocumented() {
String undocumentedContent = createHandler("<a><b>5</b></a>")
.getUndocumentedContent(Arrays.asList(
fieldWithPath("a/b").type("b").description("description"),
fieldWithPath("a").type("a").description("description")));
assertThat(undocumentedContent, is(nullValue()));
}
private XmlContentHandler createHandler(String xml) {
return new XmlContentHandler(xml.getBytes());
}
}

View File

@@ -72,6 +72,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.re
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
@@ -316,9 +317,10 @@ public class MockMvcRestDocumentationIntegrationTests {
mockMvc.perform(get("/").param("foo", "bar").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("links", responseFields(
fieldWithPath("a").description("The description"),
fieldWithPath("links").description("Links to other resources"))));
.andDo(document("links",
responseFields(fieldWithPath("a").description("The description"),
subsectionWithPath("links")
.description("Links to other resources"))));
assertExpectedSnippetFilesExist(new File("build/generated-snippets/links"),
"http-request.adoc", "http-response.adoc", "curl-request.adoc",

View File

@@ -65,6 +65,7 @@ import static org.springframework.restdocs.operation.preprocess.Preprocessors.re
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.subsectionWithPath;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.partWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
@@ -204,7 +205,7 @@ public class RestAssuredRestDocumentationIntegrationTests {
given().port(this.port).filter(documentationConfiguration(this.restDocumentation))
.filter(document("response-fields",
responseFields(fieldWithPath("a").description("The description"),
fieldWithPath("links")
subsectionWithPath("links")
.description("Links to other resources"))))
.accept("application/json").get("/").then().statusCode(200);