Skip to content

Commit

Permalink
Add support for documentation a portion of a request or response payload
Browse files Browse the repository at this point in the history
Closes gh-312
  • Loading branch information
wilkinsona committed Oct 27, 2016
1 parent 60211da commit 5e61116
Show file tree
Hide file tree
Showing 16 changed files with 1,421 additions and 28 deletions.
63 changes: 63 additions & 0 deletions docs/src/docs/asciidoc/documenting-your-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,69 @@ include::{examples-dir}/com/example/restassured/Payload.java[tags=book-array]
<1> Document the 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

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
subsection of the payload and then documenting it.

Consider the following JSON response payload:

[source,json,indent=0]
----
{
"weather": {
"wind": {
"speed": 15.3,
"direction": 287.0
},
"temperature": {
"high": 21.2,
"low": 14.8
}
}
}
----

A snippet that documents the fields of the `temperature` object (`high` and `low`) can
be produced as follows:

[source,java,indent=0,role="primary"]
.MockMvc
----
include::{examples-dir}/com/example/mockmvc/Payload.java[tags=subsection]
----
<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
`org.springframework.restdocs.payload.PayloadDocumentation`.
<2> Document the `high` and `low` fields.

[source,java,indent=0,role="secondary"]
.REST Assured
----
include::{examples-dir}/com/example/restassured/Payload.java[tags=subsection]
----
<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
`org.springframework.restdocs.payload.PayloadDocumentation`.
<2> Document the `high` and `low` fields.

The result is a snippet that contains a table describing the `high` and `low` fields
of `weather.temperature`. To make the snippet's name distinct, an identifier for the
subsection is included. By default, this identifier is `beneath-${path}`. For
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:

----
include::{examples-dir}/com/example/Payload.java[tags=custom-subsection-id]
----

This example will result in a snippet named `response-fields-temp.adoc`.



[[documenting-your-api-request-parameters]]
=== Request parameters

Expand Down
10 changes: 10 additions & 0 deletions docs/src/test/java/com/example/Payload.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

import org.springframework.restdocs.payload.FieldDescriptor;

import static org.springframework.restdocs.payload.PayloadDocumentation.beneathPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;

public class Payload {

Expand All @@ -31,4 +33,12 @@ public void bookFieldDescriptors() {
// end::book-descriptors[]
}

public void customSubsectionId() {
// tag::custom-subsection-id[]
responseFields(beneathPath("weather.temperature").withSubsectionId("temp"),
fieldWithPath("high").description("…"),
fieldWithPath("low").description("…"));
// end::custom-subsection-id[]
}

}
23 changes: 17 additions & 6 deletions docs/src/test/java/com/example/mockmvc/Payload.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,22 @@

package com.example.mockmvc;

import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
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.responseFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.snippet.Attributes.attributes;
import static org.springframework.restdocs.snippet.Attributes.key;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;

public class Payload {

private MockMvc mockMvc;
Expand Down Expand Up @@ -91,4 +92,14 @@ public void descriptorReuse() throws Exception {
// end::book-array[]
}

public void subsection() throws Exception {
// tag::subsection[]
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[]
}

}
12 changes: 12 additions & 0 deletions docs/src/test/java/com/example/restassured/Payload.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.jayway.restassured.specification.RequestSpecification;

import static org.hamcrest.CoreMatchers.is;
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.responseFields;
Expand Down Expand Up @@ -95,4 +96,15 @@ public void descriptorReuse() throws Exception {
// end::book-array[]
}

public void subsection() throws Exception {
// tag::subsection[]
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[]
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public abstract class AbstractFieldsSnippet extends TemplatedSnippet {

private final String type;

private final PayloadSubsectionExtractor<?> subsectionExtractor;

/**
* Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named
* {@code <type>-fields}. The fields will be documented using the given
Expand Down Expand Up @@ -81,6 +83,29 @@ protected AbstractFieldsSnippet(String type, List<FieldDescriptor> descriptors,
this(type, type, descriptors, attributes, ignoreUndocumentedFields);
}

/**
* Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named
* {@code <type>-fields} using a template named {@code <type>-fields}. The fields in
* the subsection of the payload extracted by the given {@code subsectionExtractor}
* will be documented using the given {@code descriptors} and the given
* {@code attributes} will be included in the model during template rendering. If
* {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be
* ignored and will not trigger a failure.
*
* @param type the type of the fields
* @param descriptors the field descriptors
* @param attributes the additional attributes
* @param ignoreUndocumentedFields whether undocumented fields should be ignored
* @param subsectionExtractor the subsection extractor
* @since 1.2.0
*/
protected AbstractFieldsSnippet(String type, List<FieldDescriptor> descriptors,
Map<String, Object> attributes, boolean ignoreUndocumentedFields,
PayloadSubsectionExtractor<?> subsectionExtractor) {
this(type, type, descriptors, attributes, ignoreUndocumentedFields,
subsectionExtractor);
}

/**
* Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named
* {@code <name>-fields} using a template named {@code <type>-fields}. The fields will
Expand All @@ -98,7 +123,35 @@ protected AbstractFieldsSnippet(String type, List<FieldDescriptor> descriptors,
protected AbstractFieldsSnippet(String name, String type,
List<FieldDescriptor> descriptors, Map<String, Object> attributes,
boolean ignoreUndocumentedFields) {
super(name + "-fields", type + "-fields", attributes);
this(name, type, descriptors, attributes, ignoreUndocumentedFields, null);
}

/**
* Creates a new {@code AbstractFieldsSnippet} that will produce a snippet named
* {@code <name>-fields} using a template named {@code <type>-fields}. The fields in
* the subsection of the payload identified by {@code subsectionPath} will be
* documented using the given {@code descriptors} and the given {@code attributes}
* will be included in the model during template rendering. If
* {@code ignoreUndocumentedFields} is {@code true}, undocumented fields will be
* ignored and will not trigger a failure.
*
* @param name the name of the snippet
* @param type the type of the fields
* @param descriptors the field descriptors
* @param attributes the additional attributes
* @param ignoreUndocumentedFields whether undocumented fields should be ignored
* @param subsectionExtractor the subsection extractor documented. {@code null} or an
* empty string can be used to indicate that the entire payload should be documented.
* @since 1.2.0
*/
protected AbstractFieldsSnippet(String name, String type,
List<FieldDescriptor> descriptors, Map<String, Object> attributes,
boolean ignoreUndocumentedFields,
PayloadSubsectionExtractor<?> subsectionExtractor) {
super(name + "-fields"
+ (subsectionExtractor != null
? "-" + subsectionExtractor.getSubsectionId() : ""),
type + "-fields", attributes);
for (FieldDescriptor descriptor : descriptors) {
Assert.notNull(descriptor.getPath(), "Field descriptors must have a path");
if (!descriptor.isIgnored()) {
Expand All @@ -112,11 +165,24 @@ protected AbstractFieldsSnippet(String name, String type,
this.fieldDescriptors = descriptors;
this.ignoreUndocumentedFields = ignoreUndocumentedFields;
this.type = type;
this.subsectionExtractor = subsectionExtractor;
}

@Override
protected Map<String, Object> createModel(Operation operation) {
ContentHandler contentHandler = getContentHandler(operation);
byte[] content;
try {
content = verifyContent(getContent(operation));
}
catch (IOException ex) {
throw new ModelCreationException(ex);
}
MediaType contentType = getContentType(operation);
if (this.subsectionExtractor != null) {
content = verifyContent(
this.subsectionExtractor.extractSubsection(content, contentType));
}
ContentHandler contentHandler = getContentHandler(content, contentType);

validateFieldDocumentation(contentHandler);

Expand Down Expand Up @@ -146,28 +212,27 @@ protected Map<String, Object> createModel(Operation operation) {
return model;
}

private ContentHandler getContentHandler(Operation operation) {
MediaType contentType = getContentType(operation);
ContentHandler contentHandler;
private byte[] verifyContent(byte[] content) {
if (content.length == 0) {
throw new SnippetException("Cannot document " + this.type + " fields as the "
+ this.type + " body is empty");
}
return content;
}

private ContentHandler getContentHandler(byte[] content, MediaType contentType) {
try {
byte[] content = getContent(operation);
if (content.length == 0) {
throw new SnippetException("Cannot document " + this.type
+ " fields as the " + this.type + " body is empty");
}
if (contentType != null
&& MediaType.APPLICATION_XML.isCompatibleWith(contentType)) {
contentHandler = new XmlContentHandler(content);
return new XmlContentHandler(content);
}
else {
contentHandler = new JsonContentHandler(content);
return new JsonContentHandler(content);
}
}
catch (IOException ex) {
throw new ModelCreationException(ex);
}
return contentHandler;
}

private void validateFieldDocumentation(ContentHandler payloadHandler) {
Expand Down
Loading

0 comments on commit 5e61116

Please sign in to comment.