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

@@ -62,26 +62,9 @@ use of HTTP status codes.
== Errors
Whenever an error response (status code >= 400) is returned, the body will contain a JSON object
that describes the problem. The error object has the following fields:
that describes the problem. The error object has the following structure:
|===
| Field | Description
| error
| The HTTP error that occurred, e.g. `Bad Request`
| message
| A description of the cause of the error
| path
| The path to which the request was made
| status
| The HTTP status code, e.g. `400`
| timestamp
| The time, in milliseconds, at which the error occurred
|===
include::{generated}/error-example/response-fields.asciidoc[]
For example, a request that attempts to apply a non-existent tag to a note will produce a
`400 Bad Request` response:
@@ -116,12 +99,7 @@ A `GET` request is used to access the index
==== Response structure
|===
| JSON path | Description
| `_links`
| <<resources-index-links,Links>> to other resources
|===
include::{generated}/index-example/response-fields.asciidoc[]
==== Example response
@@ -132,15 +110,7 @@ include::{generated}/index-example/response.asciidoc[]
[[resources-index-links]]
==== Links
|===
| Relation | Description
| notes
| The <<resources-notes,Notes resource>>
| tags
| The <<resources-tags,Tags resource>>
|===
include::{generated}/index-example/links.asciidoc[]
@@ -158,12 +128,7 @@ A `GET` request will list all of the service's notes.
==== Response structure
|===
| JSON path | Description
| `_embedded.notes`
| An array of <<resources-note,Note resources>>
|===
include::{generated}/notes-list-example/response-fields.asciidoc[]
==== Example request
@@ -182,18 +147,7 @@ A `POST` request is used to create a note
==== Request structure
|===
| JSON path | Description
| `title`
| The title of the note
| `body`
| The body of the note
| `tags`
| | The tags of the note as an array of URIs
|===
include::{generated}/notes-create-example/request-fields.asciidoc[]
==== Example request
@@ -219,12 +173,7 @@ A `GET` request will list all of the service's tags.
==== Response structure
|===
| JSON path | Description
| `_embedded.tags`
| An array of <<resources-tag,Tag resources>>
|===
include::{generated}/tags-list-example/response-fields.asciidoc[]
==== Example request
@@ -243,12 +192,7 @@ A `POST` request is used to create a note
==== Request structure
|===
| JSON path | Description
| `name`
| The name of the tag
|===
include::{generated}/tags-create-example/request-fields.asciidoc[]
==== Example request
@@ -270,15 +214,7 @@ The Note resource is used to retrieve, update, and delete individual notes
[[resources-note-links]]
=== Links
|===
| Relation | Description
| self
| This <<resources-note,note>>
| note-tags
| This note's <<resources-tags,tags>>
|===
include::{generated}/note-get-example/links.asciidoc[]
@@ -287,23 +223,18 @@ The Note resource is used to retrieve, update, and delete individual notes
A `GET` request will retrieve the details of a note
Example response:
==== Response structure
include::{generated}/note-get-example/response-fields.asciidoc[]
==== Example request
include::{generated}/note-get-example/request.asciidoc[]
==== Example response
include::{generated}/note-get-example/response.asciidoc[]
|===
| JSON path | Description
| `title`
| The title of the note
| `body`
| The body of the note
| `_links`
| <<resources-note-links,Links>> to other resources
|===
[[resources-note-update]]
@@ -313,18 +244,7 @@ A `PATCH` request is used to update a note
==== Request structure
|===
| JSON path | Description
| `title`
| The title of the note
| `body`
| The body of the note
| `tags`
| The tags of the note as an array of URIs
|===
include::{generated}/note-update-example/request-fields.asciidoc[]
To leave an attribute of a note unchanged, any of the above may be omitted from the request.
@@ -337,7 +257,8 @@ include::{generated}/note-update-example/request.asciidoc[]
include::{generated}/note-update-example/response.asciidoc[]
[[resources-note]]
[[resources-tag]]
== Tag
The Tag resource is used to retrieve, update, and delete individual tags
@@ -347,15 +268,7 @@ The Tag resource is used to retrieve, update, and delete individual tags
[[resources-tag-links]]
=== Links
|===
| Relation | Description
| self
| This <<resources-tag,tag>>
| notes
| The <<resources-notes,notes>> that have this tag
|===
include:{{generated}/tag-get-example/links.asciidoc
@@ -364,20 +277,18 @@ The Tag resource is used to retrieve, update, and delete individual tags
A `GET` request will retrieve the details of a tag
Example response:
==== Response structure
include::{generated}/tag-get-example/response-fields.asciidoc[]
==== Example request
include::{generated}/tag-get-example/request.asciidoc[]
==== Example response
include::{generated}/tag-get-example/response.asciidoc[]
|===
| JSON path | Description
| `name`
| The name of the tag
| `_links`
| <<resources-tag-links,Links>> to other resources
|===
[[resources-tag-update]]
@@ -387,13 +298,7 @@ A `PATCH` request is used to update a tag
==== Request structure
|===
| JSON path | Description
| `name`
| The name of the tag
|===
include::{generated}/tag-update-example/request-fields.asciidoc[]
==== Example request

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) {