Improve support for documenting fields in req and resp payloads

This commit improves the support for documenting the fields found
in request and response payloads. The improvements include:

 - Using the term "payload" rather than "state" as the latter is
   somewhat HAL-specific
 - Code simplification, including the removal of the Path abstraction
 - Automatically determining the type of a field based on the payload
   that's being tested
 - Updating the samples to use the new API

In addition to these improvements, support for documenting field
constraints has been removed as more thought is required. The
FieldDescriptor abstraction is used to describe both request and
response fields, however it's not clear that contraints apply to both
and they certainly don't apply in the same way. For the time being at
least, users who want to document a field's constraints can do so as
part of its description.

Closes gh-24
This commit is contained in:
Andy Wilkinson
2015-03-23 18:03:17 +00:00
parent ecfc7abd92
commit 2b44452de1
23 changed files with 857 additions and 1056 deletions

View File

@@ -20,6 +20,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.restdocs.RestDocumentation.document;
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.linkWithRel;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@@ -40,6 +41,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.hateoas.MediaTypes;
import org.springframework.restdocs.config.RestDocumentationConfigurer;
import org.springframework.restdocs.payload.FieldType;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
@@ -87,20 +89,26 @@ public class ApiDocumentation {
.andExpect(jsonPath("timestamp", is(notNullValue())))
.andExpect(jsonPath("status", is(400)))
.andExpect(jsonPath("path", is(notNullValue())))
.andDo(document("error-example"));
.andDo(document("error-example")
.withResponseFields(
fieldWithPath("error").description("The HTTP error that occurred, e.g. `Bad Request`"),
fieldWithPath("message").description("A description of the cause of the error"),
fieldWithPath("path").description("The path to which the request was made"),
fieldWithPath("status").description("The HTTP status code, e.g. `400`"),
fieldWithPath("timestamp").description("The time, in milliseconds, at which the error occurred")));
}
@Test
public void indexExample() throws Exception {
this.mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andDo(document("index-example").withLinks(
linkWithRel("notes").description(
"The <<resources-notes,Notes resource>>"),
linkWithRel("tags").description(
"The <<resources-tags,Tags resource>>"),
linkWithRel("profile").description(
"The ALPS profile for the service")));
.andDo(document("index-example")
.withLinks(
linkWithRel("notes").description("The <<resources-notes,Notes resource>>"),
linkWithRel("tags").description("The <<resources-tags,Tags resource>>"),
linkWithRel("profile").description("The ALPS profile for the service"))
.withResponseFields(
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources")));
}
@@ -116,7 +124,9 @@ public class ApiDocumentation {
this.mockMvc.perform(get("/notes"))
.andExpect(status().isOk())
.andDo(document("notes-list-example"));
.andDo(document("notes-list-example")
.withResponseFields(
fieldWithPath("_embedded.notes").description("An array of <<resources-note, Note resources>>")));
}
@Test
@@ -140,7 +150,11 @@ public class ApiDocumentation {
post("/notes").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(note))).andExpect(
status().isCreated())
.andDo(document("notes-create-example"));
.andDo(document("notes-create-example")
.withRequestFields(
fieldWithPath("title").description("The title of the note"),
fieldWithPath("body").description("The body of the note"),
fieldWithPath("tags").description("An array of tag resource URIs")));
}
@Test
@@ -176,9 +190,11 @@ public class ApiDocumentation {
.andDo(document("note-get-example")
.withLinks(
linkWithRel("self").description("This <<resources-note,note>>"),
linkWithRel("tags").description(
"This note's <<resources-note-tags,tags>>")));
linkWithRel("tags").description("This note's tags"))
.withResponseFields(
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")));
}
@Test
@@ -192,7 +208,9 @@ public class ApiDocumentation {
this.mockMvc.perform(get("/tags"))
.andExpect(status().isOk())
.andDo(document("tags-list-example"));
.andDo(document("tags-list-example")
.withResponseFields(
fieldWithPath("_embedded.tags").description("An array of <<resources-tag,Tag resources>>")));
}
@Test
@@ -202,9 +220,11 @@ public class ApiDocumentation {
this.mockMvc.perform(
post("/tags").contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tag))).andExpect(
status().isCreated())
.andDo(document("tags-create-example"));
this.objectMapper.writeValueAsString(tag)))
.andExpect(status().isCreated())
.andDo(document("tags-create-example")
.withRequestFields(
fieldWithPath("name").description("The name of the tag")));
}
@Test
@@ -243,7 +263,12 @@ public class ApiDocumentation {
patch(noteLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(noteUpdate)))
.andExpect(status().isNoContent())
.andDo(document("note-update-example"));
.andDo(document("note-update-example")
.withRequestFields(
fieldWithPath("title").description("The title of the note").type(FieldType.STRING).optional(),
fieldWithPath("body").description("The body of the note").type(FieldType.STRING).optional(),
fieldWithPath("tags").description("An array of tag resource URIs").optional()));
}
@Test
@@ -261,11 +286,13 @@ public class ApiDocumentation {
this.mockMvc.perform(get(tagLocation))
.andExpect(status().isOk())
.andExpect(jsonPath("name", is(tag.get("name"))))
.andDo(document("tag-get-example").withLinks(
linkWithRel("self").description("This <<resources-tag,tag>>"),
linkWithRel("notes")
.description(
"The <<resources-tagged-notes,notes>> that have this tag")));
.andDo(document("tag-get-example")
.withLinks(
linkWithRel("self").description("This <<resources-tag,tag>>"),
linkWithRel("notes").description("The <<resources-tagged-notes,notes>> that have this tag"))
.withResponseFields(
fieldWithPath("name").description("The name of the tag"),
fieldWithPath("_links").description("<<resources-tag-links,Links>> to other resources")));
}
@Test
@@ -287,7 +314,9 @@ public class ApiDocumentation {
patch(tagLocation).contentType(MediaTypes.HAL_JSON).content(
this.objectMapper.writeValueAsString(tagUpdate)))
.andExpect(status().isNoContent())
.andDo(document("tag-update-example"));
.andDo(document("tag-update-example")
.withRequestFields(
fieldWithPath("name").description("The name of the tag")));
}
private void createNote(String title, String body) {