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