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:
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -150,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
|
||||
|
||||
@@ -174,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
|
||||
|
||||
@@ -211,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
|
||||
|
||||
@@ -235,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
|
||||
|
||||
@@ -271,23 +223,18 @@ include::{generated}/note-get-example/links.asciidoc[]
|
||||
|
||||
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]]
|
||||
@@ -297,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.
|
||||
|
||||
@@ -321,7 +257,7 @@ 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
|
||||
@@ -340,20 +276,18 @@ include::{generated}/tag-get-example/links.asciidoc[]
|
||||
|
||||
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]]
|
||||
@@ -363,13 +297,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
|
||||
|
||||
|
||||
@@ -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,18 +89,25 @@ 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>>")));
|
||||
.andDo(document("index-example")
|
||||
.withLinks(
|
||||
linkWithRel("notes").description("The <<resources-notes,Notes resource>>"),
|
||||
linkWithRel("tags").description("The <<resources-tags,Tags resource>>"))
|
||||
.withResponseFields(
|
||||
fieldWithPath("_links").description("<<resources-index-links,Links>> to other resources")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -113,7 +122,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
|
||||
@@ -137,7 +148,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
|
||||
@@ -170,10 +185,14 @@ public class ApiDocumentation {
|
||||
.andExpect(jsonPath("body", is(note.get("body"))))
|
||||
.andExpect(jsonPath("_links.self.href", is(noteLocation)))
|
||||
.andExpect(jsonPath("_links.note-tags", is(notNullValue())))
|
||||
.andDo(document("note-get-example").withLinks(
|
||||
linkWithRel("self").description("This <<resources-note,note>>"),
|
||||
linkWithRel("note-tags").description(
|
||||
"This note's <<resources-note-tags,tags>>")));
|
||||
.andDo(document("note-get-example")
|
||||
.withLinks(
|
||||
linkWithRel("self").description("This <<resources-note,note>>"),
|
||||
linkWithRel("note-tags").description("This note's <<resources-note-tags,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")));
|
||||
|
||||
}
|
||||
|
||||
@@ -188,7 +207,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
|
||||
@@ -200,7 +221,9 @@ public class ApiDocumentation {
|
||||
post("/tags").contentType(MediaTypes.HAL_JSON).content(
|
||||
this.objectMapper.writeValueAsString(tag)))
|
||||
.andExpect(status().isCreated())
|
||||
.andDo(document("tags-create-example"));
|
||||
.andDo(document("tags-create-example")
|
||||
.withRequestFields(
|
||||
fieldWithPath("name").description("The name of the tag")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -239,7 +262,11 @@ 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
|
||||
@@ -257,11 +284,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("tagged-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("tagged-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
|
||||
@@ -283,7 +312,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) {
|
||||
|
||||
@@ -20,8 +20,8 @@ import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRe
|
||||
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlRequestAndResponse;
|
||||
import static org.springframework.restdocs.curl.CurlDocumentation.documentCurlResponse;
|
||||
import static org.springframework.restdocs.hypermedia.HypermediaDocumentation.documentLinks;
|
||||
import static org.springframework.restdocs.state.StateDocumentation.documentRequestFields;
|
||||
import static org.springframework.restdocs.state.StateDocumentation.documentResponseFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.documentRequestFields;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.documentResponseFields;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -30,9 +30,8 @@ import org.springframework.restdocs.hypermedia.HypermediaDocumentation;
|
||||
import org.springframework.restdocs.hypermedia.LinkDescriptor;
|
||||
import org.springframework.restdocs.hypermedia.LinkExtractor;
|
||||
import org.springframework.restdocs.hypermedia.LinkExtractors;
|
||||
import org.springframework.restdocs.state.FieldDescriptor;
|
||||
import org.springframework.restdocs.state.Path;
|
||||
import org.springframework.restdocs.state.StateDocumentation;
|
||||
import org.springframework.restdocs.payload.FieldDescriptor;
|
||||
import org.springframework.restdocs.payload.PayloadDocumentation;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultHandler;
|
||||
|
||||
@@ -104,17 +103,16 @@ public class RestDocumentationResultHandler implements ResultHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Document the fields in the response using the given {@code descriptors}. The fields
|
||||
* are extracted from the response based on its content type.
|
||||
* Document the fields in the request using the given {@code descriptors}.
|
||||
* <p>
|
||||
* If a field is present in the response but is not described by one of the
|
||||
* descriptors a failure will occur when this handler is invoked. Similarly, if a
|
||||
* field is described but is not present in the response a failure will also occur
|
||||
* when this handler is invoked.
|
||||
* If a field is present in the request but is not described by one of the descriptors
|
||||
* a failure will occur when this handler is invoked. Similarly, if a field is
|
||||
* described but is not present in the request a failure will also occur when this
|
||||
* handler is invoked.
|
||||
*
|
||||
* @param descriptors the link descriptors
|
||||
* @return {@code this}
|
||||
* @see StateDocumentation#fieldWithPath(Path)
|
||||
* @see PayloadDocumentation#fieldWithPath(String)
|
||||
*/
|
||||
public RestDocumentationResultHandler withRequestFields(
|
||||
FieldDescriptor... descriptors) {
|
||||
@@ -123,8 +121,7 @@ public class RestDocumentationResultHandler implements ResultHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Document the fields in the response using the given {@code descriptors}. The fields
|
||||
* are extracted from the response based on its content type.
|
||||
* Document the fields in the response using the given {@code descriptors}.
|
||||
* <p>
|
||||
* If a field is present in the response but is not described by one of the
|
||||
* descriptors a failure will occur when this handler is invoked. Similarly, if a
|
||||
@@ -133,7 +130,7 @@ public class RestDocumentationResultHandler implements ResultHandler {
|
||||
*
|
||||
* @param descriptors the link descriptors
|
||||
* @return {@code this}
|
||||
* @see StateDocumentation#fieldWithPath(Path)
|
||||
* @see PayloadDocumentation#fieldWithPath(String)
|
||||
*/
|
||||
public RestDocumentationResultHandler withResponseFields(
|
||||
FieldDescriptor... descriptors) {
|
||||
|
||||
@@ -14,61 +14,49 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.restdocs.state;
|
||||
package org.springframework.restdocs.payload;
|
||||
|
||||
/**
|
||||
* A description of a field found in a hypermedia API
|
||||
* A description of a field found in a request or response payload
|
||||
*
|
||||
* @see StateDocumentation#fieldWithPath(Path)
|
||||
* @see PayloadDocumentation#fieldWithPath(String)
|
||||
*
|
||||
* @author Andreas Evers
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class FieldDescriptor {
|
||||
|
||||
private final Path path;
|
||||
private final String path;
|
||||
|
||||
private String type;
|
||||
private FieldType type;
|
||||
|
||||
private boolean required;
|
||||
|
||||
private String constraints;
|
||||
private boolean optional;
|
||||
|
||||
private String description;
|
||||
|
||||
FieldDescriptor(Path path) {
|
||||
FieldDescriptor(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the type of the field
|
||||
*
|
||||
* @param type The field's type (could be number, string, boolean, array, object, ...)
|
||||
* @param type The type of the field
|
||||
*
|
||||
* @return {@code this}
|
||||
*/
|
||||
public FieldDescriptor type(String type) {
|
||||
public FieldDescriptor type(FieldType type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies necessity of the field
|
||||
* Marks the field as optional
|
||||
*
|
||||
* @param required The field's necessity
|
||||
* @return {@code this}
|
||||
*/
|
||||
public FieldDescriptor required(boolean required) {
|
||||
this.required = required;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the constraints of the field
|
||||
*
|
||||
* @param constraints The field's constraints
|
||||
* @return {@code this}
|
||||
*/
|
||||
public FieldDescriptor constraints(String constraints) {
|
||||
this.constraints = constraints;
|
||||
public FieldDescriptor optional() {
|
||||
this.optional = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -83,20 +71,16 @@ public class FieldDescriptor {
|
||||
return this;
|
||||
}
|
||||
|
||||
Path getPath() {
|
||||
String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
String getType() {
|
||||
FieldType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
boolean isRequired() {
|
||||
return this.required;
|
||||
}
|
||||
|
||||
String getConstraints() {
|
||||
return this.constraints;
|
||||
boolean isOptional() {
|
||||
return this.optional;
|
||||
}
|
||||
|
||||
String getDescription() {
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.springframework.restdocs.payload;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A {@link FieldExtractor} extracts a field from a payload
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*
|
||||
*/
|
||||
class FieldExtractor {
|
||||
|
||||
boolean hasField(String path, Map<String, Object> payload) {
|
||||
String[] segments = path.indexOf('.') > -1 ? path.split("\\.")
|
||||
: new String[] { path };
|
||||
|
||||
Object current = payload;
|
||||
|
||||
for (String segment : segments) {
|
||||
if (current instanceof Map && ((Map<?, ?>) current).containsKey(segment)) {
|
||||
current = ((Map<?, ?>) current).get(segment);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Object extractField(String path, Map<String, Object> payload) {
|
||||
String[] segments = path.indexOf('.') > -1 ? path.split("\\.")
|
||||
: new String[] { path };
|
||||
|
||||
Object current = payload;
|
||||
|
||||
for (String segment : segments) {
|
||||
if (current instanceof Map && ((Map<?, ?>) current).containsKey(segment)) {
|
||||
current = ((Map<?, ?>) current).get(segment);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException(
|
||||
"The payload does not contain a field with the path '" + path
|
||||
+ "'");
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.springframework.restdocs.snippet.DocumentationWriter;
|
||||
import org.springframework.restdocs.snippet.DocumentationWriter.TableAction;
|
||||
import org.springframework.restdocs.snippet.DocumentationWriter.TableWriter;
|
||||
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful
|
||||
* resource's request or response fields.
|
||||
*
|
||||
* @author Andreas Evers
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class FieldSnippetResultHandler extends SnippetWritingResultHandler {
|
||||
|
||||
private final Map<String, FieldDescriptor> descriptorsByPath = new LinkedHashMap<String, FieldDescriptor>();
|
||||
|
||||
private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver();
|
||||
|
||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
||||
|
||||
private final FieldValidator fieldValidator = new FieldValidator();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private List<FieldDescriptor> fieldDescriptors;
|
||||
|
||||
FieldSnippetResultHandler(String outputDir, String filename,
|
||||
List<FieldDescriptor> descriptors) {
|
||||
super(outputDir, filename + "-fields");
|
||||
for (FieldDescriptor descriptor : descriptors) {
|
||||
Assert.notNull(descriptor.getPath());
|
||||
Assert.hasText(descriptor.getDescription());
|
||||
this.descriptorsByPath.put(descriptor.getPath(), descriptor);
|
||||
}
|
||||
this.fieldDescriptors = descriptors;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(MvcResult result, DocumentationWriter writer)
|
||||
throws IOException {
|
||||
|
||||
this.fieldValidator.validate(getPayloadReader(result), this.fieldDescriptors);
|
||||
|
||||
final Map<String, Object> payload = extractPayload(result);
|
||||
|
||||
List<String> missingFields = new ArrayList<String>();
|
||||
|
||||
for (FieldDescriptor fieldDescriptor : this.fieldDescriptors) {
|
||||
if (!fieldDescriptor.isOptional()) {
|
||||
Object field = this.fieldExtractor.extractField(
|
||||
fieldDescriptor.getPath(), payload);
|
||||
if (field == null) {
|
||||
missingFields.add(fieldDescriptor.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.table(new TableAction() {
|
||||
|
||||
@Override
|
||||
public void perform(TableWriter tableWriter) throws IOException {
|
||||
tableWriter.headers("Path", "Type", "Description");
|
||||
for (Entry<String, FieldDescriptor> entry : FieldSnippetResultHandler.this.descriptorsByPath
|
||||
.entrySet()) {
|
||||
FieldDescriptor descriptor = entry.getValue();
|
||||
FieldType type = descriptor.getType() != null ? descriptor.getType()
|
||||
: FieldSnippetResultHandler.this.fieldTypeResolver
|
||||
.resolveFieldType(descriptor.getPath(), payload);
|
||||
tableWriter.row(entry.getKey().toString(), type.toString(), entry
|
||||
.getValue().getDescription());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> extractPayload(MvcResult result) throws IOException {
|
||||
Reader payloadReader = getPayloadReader(result);
|
||||
try {
|
||||
return this.objectMapper.readValue(payloadReader, Map.class);
|
||||
}
|
||||
finally {
|
||||
payloadReader.close();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Reader getPayloadReader(MvcResult result) throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.Locale;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An enumeration of the possible types for a field in a JSON request or response payload
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public enum FieldType {
|
||||
|
||||
ARRAY, BOOLEAN, OBJECT, NUMBER, NULL, STRING;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return StringUtils.capitalize(this.name().toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Resolves the type of a field in a request or response payload
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class FieldTypeResolver {
|
||||
|
||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
||||
|
||||
FieldType resolveFieldType(String path, Map<String, Object> payload) {
|
||||
return determineFieldType(this.fieldExtractor.extractField(path, payload));
|
||||
}
|
||||
|
||||
private FieldType determineFieldType(Object fieldValue) {
|
||||
if (fieldValue == null) {
|
||||
return FieldType.NULL;
|
||||
}
|
||||
if (fieldValue instanceof String) {
|
||||
return FieldType.STRING;
|
||||
}
|
||||
if (fieldValue instanceof Map) {
|
||||
return FieldType.OBJECT;
|
||||
}
|
||||
if (fieldValue instanceof Collection) {
|
||||
return FieldType.ARRAY;
|
||||
}
|
||||
if (fieldValue instanceof Boolean) {
|
||||
return FieldType.BOOLEAN;
|
||||
}
|
||||
return FieldType.NUMBER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
/**
|
||||
* {@code FieldValidator} is used to validate a payload's fields against the user-provided
|
||||
* {@link FieldDescriptor}s.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class FieldValidator {
|
||||
|
||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper()
|
||||
.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
void validate(Reader payloadReader, List<FieldDescriptor> fieldDescriptors)
|
||||
throws IOException {
|
||||
Map<String, Object> payload = this.objectMapper.readValue(payloadReader,
|
||||
Map.class);
|
||||
List<String> missingFields = findMissingFields(payload, fieldDescriptors);
|
||||
Map<String, Object> undocumentedPayload = findUndocumentedFields(payload,
|
||||
fieldDescriptors);
|
||||
|
||||
if (!missingFields.isEmpty() || !undocumentedPayload.isEmpty()) {
|
||||
String message = "";
|
||||
if (!undocumentedPayload.isEmpty()) {
|
||||
message += String.format(
|
||||
"Portions of the payload were not documented:%n%s",
|
||||
this.objectMapper.writeValueAsString(undocumentedPayload));
|
||||
}
|
||||
if (!missingFields.isEmpty()) {
|
||||
message += "Fields with the following paths were not found in the payload: "
|
||||
+ missingFields;
|
||||
}
|
||||
throw new FieldValidationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> findMissingFields(Map<String, Object> payload,
|
||||
List<FieldDescriptor> fieldDescriptors) {
|
||||
List<String> missingFields = new ArrayList<String>();
|
||||
|
||||
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
||||
if (!fieldDescriptor.isOptional()) {
|
||||
if (!this.fieldExtractor.hasField(fieldDescriptor.getPath(), payload)) {
|
||||
missingFields.add(fieldDescriptor.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return missingFields;
|
||||
}
|
||||
|
||||
private Map<String, Object> findUndocumentedFields(Map<String, Object> payload,
|
||||
List<FieldDescriptor> fieldDescriptors) {
|
||||
for (FieldDescriptor fieldDescriptor : fieldDescriptors) {
|
||||
String path = fieldDescriptor.getPath();
|
||||
List<String> segments = path.indexOf('.') > -1 ? Arrays.asList(path
|
||||
.split("\\.")) : Arrays.asList(path);
|
||||
removeField(segments, 0, payload);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private void removeField(List<String> segments, int depth,
|
||||
Map<String, Object> payloadPortion) {
|
||||
String key = segments.get(depth);
|
||||
if (depth == segments.size() - 1) {
|
||||
payloadPortion.remove(key);
|
||||
}
|
||||
else {
|
||||
Object candidate = payloadPortion.get(key);
|
||||
if (candidate instanceof Map) {
|
||||
Map map = (Map<?, ?>) candidate;
|
||||
removeField(segments, depth + 1, map);
|
||||
if (map.isEmpty()) {
|
||||
payloadPortion.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class FieldValidationException extends RuntimeException {
|
||||
|
||||
FieldValidationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,24 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.restdocs.state;
|
||||
|
||||
import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.REQUEST;
|
||||
import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.RESPONSE;
|
||||
import static org.springframework.restdocs.state.Path.path;
|
||||
package org.springframework.restdocs.payload;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.springframework.restdocs.RestDocumentationResultHandler;
|
||||
|
||||
/**
|
||||
* Static factory methods for documenting a RESTful API's state.
|
||||
* Static factory methods for documenting a RESTful API's request and response payloads.
|
||||
*
|
||||
* @author Andreas Evers
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public abstract class StateDocumentation {
|
||||
public abstract class PayloadDocumentation {
|
||||
|
||||
private StateDocumentation() {
|
||||
private PayloadDocumentation() {
|
||||
|
||||
}
|
||||
|
||||
@@ -44,23 +41,10 @@ public abstract class StateDocumentation {
|
||||
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
|
||||
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
|
||||
*/
|
||||
public static FieldDescriptor fieldWithPath(Path path) {
|
||||
public static FieldDescriptor fieldWithPath(String path) {
|
||||
return new FieldDescriptor(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code FieldDescriptor} that describes a field with the given
|
||||
* {@code path}, in case the field is at the root of the request or response body
|
||||
*
|
||||
* @param name The name of the field being at the root of the request or response body
|
||||
* @return a {@code FieldDescriptor} ready for further configuration
|
||||
* @see RestDocumentationResultHandler#withRequestFields(FieldDescriptor...)
|
||||
* @see RestDocumentationResultHandler#withResponseFields(FieldDescriptor...)
|
||||
*/
|
||||
public static FieldDescriptor fieldWithPath(String name) {
|
||||
return new FieldDescriptor(path(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code RequestFieldsSnippetResultHandler} that will produce a
|
||||
* documentation snippet for a request's fields.
|
||||
@@ -72,8 +56,7 @@ public abstract class StateDocumentation {
|
||||
*/
|
||||
public static FieldSnippetResultHandler documentRequestFields(String outputDir,
|
||||
FieldDescriptor... descriptors) {
|
||||
return new FieldSnippetResultHandler(outputDir, REQUEST,
|
||||
Arrays.asList(descriptors));
|
||||
return new RequestFieldSnippetResultHandler(outputDir, Arrays.asList(descriptors));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +70,7 @@ public abstract class StateDocumentation {
|
||||
*/
|
||||
public static FieldSnippetResultHandler documentResponseFields(String outputDir,
|
||||
FieldDescriptor... descriptors) {
|
||||
return new FieldSnippetResultHandler(outputDir, RESPONSE,
|
||||
return new ResponseFieldSnippetResultHandler(outputDir,
|
||||
Arrays.asList(descriptors));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
/**
|
||||
* A {@link FieldSnippetResultHandler} for documenting a request's fields
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class RequestFieldSnippetResultHandler extends FieldSnippetResultHandler {
|
||||
|
||||
RequestFieldSnippetResultHandler(String outputDir, List<FieldDescriptor> descriptors) {
|
||||
super(outputDir, "request", descriptors);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Reader getPayloadReader(MvcResult result) throws IOException {
|
||||
return result.getRequest().getReader();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
/**
|
||||
* A {@link FieldSnippetResultHandler} for documenting a response's fields
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class ResponseFieldSnippetResultHandler extends FieldSnippetResultHandler {
|
||||
|
||||
ResponseFieldSnippetResultHandler(String outputDir, List<FieldDescriptor> descriptors) {
|
||||
super(outputDir, "response", descriptors);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Reader getPayloadReader(MvcResult result) throws IOException {
|
||||
return new StringReader(result.getResponse().getContentAsString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.state;
|
||||
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
|
||||
/**
|
||||
* Representation of a field used in a Hypermedia-based API
|
||||
*
|
||||
* @author Andreas Evers
|
||||
*/
|
||||
public class Field {
|
||||
|
||||
private final Path path;
|
||||
|
||||
private final Object value;
|
||||
|
||||
/**
|
||||
* Creates a new {@code Field} with the given {@code path} and {@code value}
|
||||
*
|
||||
* @param path The field's path
|
||||
* @param value The field's value
|
||||
*/
|
||||
public Field(Path path, Object value) {
|
||||
this.path = path;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field's {@code path}
|
||||
* @return the field's {@code path}
|
||||
*/
|
||||
public Path getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field's {@code value}
|
||||
* @return the field's {@code value}
|
||||
*/
|
||||
public Object getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + this.path.hashCode();
|
||||
result = prime * result + this.value.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Field other = (Field) obj;
|
||||
if (!this.path.equals(other.path)) {
|
||||
return false;
|
||||
}
|
||||
if (!this.value.equals(other.value)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this).append("path", this.path)
|
||||
.append("value", this.value).toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.state;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* A {@code FieldExtractor} is used to extract {@link Field fields} from a JSON response.
|
||||
* The expected format of the links in the response is determined by the implementation.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*
|
||||
*/
|
||||
public class FieldExtractor {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private Map<Path, Field> extractedFields = new HashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<Path, Field> extractFields(MockHttpServletRequest request)
|
||||
throws IOException {
|
||||
Map<String, Object> jsonContent = this.objectMapper.readValue(
|
||||
request.getInputStream(), Map.class);
|
||||
extractFieldsRecursively(jsonContent);
|
||||
return this.extractedFields;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<Path, Field> extractFields(MockHttpServletResponse response)
|
||||
throws IOException {
|
||||
String responseBody = response.getContentAsString();
|
||||
Assert.hasText(responseBody,
|
||||
"The response doesn't contain a body to extract fields from");
|
||||
Map<String, Object> jsonContent = this.objectMapper.readValue(responseBody,
|
||||
Map.class);
|
||||
extractFieldsRecursively(jsonContent);
|
||||
return this.extractedFields;
|
||||
}
|
||||
|
||||
private void extractFieldsRecursively(Map<String, Object> jsonContent) {
|
||||
extractFieldsRecursively(null, jsonContent);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void extractFieldsRecursively(Path previousSteps,
|
||||
Map<String, Object> jsonContent) {
|
||||
for (Entry<String, Object> entry : jsonContent.entrySet()) {
|
||||
Path path;
|
||||
if (previousSteps == null) {
|
||||
path = new Path(entry.getKey());
|
||||
}
|
||||
else {
|
||||
path = new Path(previousSteps, entry.getKey());
|
||||
}
|
||||
this.extractedFields.put(path, new Field(path, entry.getValue()));
|
||||
if (entry.getValue() instanceof Map) {
|
||||
Map<String, Object> value = (Map<String, Object>) entry.getValue();
|
||||
extractFieldsRecursively(path, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.state;
|
||||
|
||||
import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.REQUEST;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.springframework.restdocs.snippet.DocumentationWriter;
|
||||
import org.springframework.restdocs.snippet.SnippetWritingResultHandler;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link SnippetWritingResultHandler} that produces a snippet documenting a RESTful
|
||||
* resource's request or response fields.
|
||||
*
|
||||
* @author Andreas Evers
|
||||
*/
|
||||
public class FieldSnippetResultHandler extends SnippetWritingResultHandler {
|
||||
|
||||
private final Map<Path, FieldDescriptor> descriptorsByName = new HashMap<Path, FieldDescriptor>();
|
||||
|
||||
private final Type type;
|
||||
|
||||
private FieldExtractor extractor = new FieldExtractor();
|
||||
|
||||
private StateDocumentationValidator validator;
|
||||
|
||||
enum Type {
|
||||
REQUEST, RESPONSE;
|
||||
}
|
||||
|
||||
FieldSnippetResultHandler(String outputDir, FieldSnippetResultHandler.Type type,
|
||||
List<FieldDescriptor> descriptors) {
|
||||
super(outputDir, type.toString().toLowerCase() + "fields");
|
||||
this.type = type;
|
||||
for (FieldDescriptor descriptor : descriptors) {
|
||||
Assert.notNull(descriptor.getPath());
|
||||
Assert.hasText(descriptor.getDescription());
|
||||
this.descriptorsByName.put(descriptor.getPath(), descriptor);
|
||||
}
|
||||
this.validator = new StateDocumentationValidator(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handle(MvcResult result, DocumentationWriter writer)
|
||||
throws IOException {
|
||||
Map<Path, Field> fields;
|
||||
if (this.type == REQUEST) {
|
||||
fields = this.extractor.extractFields(result.getRequest());
|
||||
}
|
||||
else {
|
||||
fields = this.extractor.extractFields(result.getResponse());
|
||||
}
|
||||
|
||||
SortedSet<Path> actualFields = new TreeSet<Path>(fields.keySet());
|
||||
SortedSet<Path> expectedFields = new TreeSet<Path>(
|
||||
this.descriptorsByName.keySet());
|
||||
|
||||
this.validator.validateFields(actualFields, expectedFields);
|
||||
|
||||
writer.println("|===");
|
||||
writer.println("| Path | Description | Type | Required | Constraints");
|
||||
|
||||
for (Entry<Path, FieldDescriptor> entry : this.descriptorsByName.entrySet()) {
|
||||
writer.println();
|
||||
writer.println("| " + entry.getKey());
|
||||
writer.println("| " + entry.getValue().getDescription());
|
||||
writer.println("| " + entry.getValue().getType());
|
||||
writer.println("| " + entry.getValue().isRequired());
|
||||
writer.println("| " + entry.getValue().getConstraints());
|
||||
}
|
||||
|
||||
writer.println("|===");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.state;
|
||||
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
|
||||
/**
|
||||
* Representation of a path for a field. In case the field is a nested field, there will
|
||||
* be multiple steps. Each step is the name of a field, albeit parents of the field in
|
||||
* question. In case the field is not nested, there is only one step, which is the name of the field.
|
||||
*
|
||||
* @author Andreas Evers
|
||||
*/
|
||||
public class Path implements Comparable<Path> {
|
||||
|
||||
private List<String> steps = new ArrayList<>();
|
||||
|
||||
public Path(Path path) {
|
||||
this.steps = new ArrayList<String>(path.getSteps());
|
||||
}
|
||||
|
||||
public Path(List<String> steps) {
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public Path(Path previousSteps, String newStep) {
|
||||
this.steps.addAll(previousSteps.getSteps());
|
||||
this.steps.add(newStep);
|
||||
}
|
||||
|
||||
public Path(String... steps) {
|
||||
this.steps = Arrays.asList(steps);
|
||||
}
|
||||
|
||||
public static Path path(List<String> steps) {
|
||||
return new Path(steps);
|
||||
}
|
||||
|
||||
public static Path path(String... steps) {
|
||||
return new Path(steps);
|
||||
}
|
||||
|
||||
public static Path path(Path previousSteps, String newStep) {
|
||||
return new Path(previousSteps, newStep);
|
||||
}
|
||||
|
||||
public List<String> getSteps() {
|
||||
return this.steps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((this.steps == null) ? 0 : this.steps.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Path other = (Path) obj;
|
||||
if (this.steps == null) {
|
||||
if (other.steps != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!this.steps.equals(other.steps)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this).append("steps", this.steps).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Path o) {
|
||||
int comparison = 0;
|
||||
int size = min(this.steps.size(), o.getSteps().size());
|
||||
for (int i = 0; i < size; i++) {
|
||||
String thisStep = this.steps.get(i);
|
||||
String thatStep = o.getSteps().get(i);
|
||||
comparison = thisStep.compareTo(thatStep);
|
||||
if (comparison != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (comparison == 0) {
|
||||
comparison = ((Integer) this.steps.size()).compareTo(o.getSteps().size());
|
||||
}
|
||||
return comparison;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.state;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
|
||||
import org.springframework.restdocs.state.FieldSnippetResultHandler.Type;
|
||||
|
||||
/**
|
||||
* Validator which verifies if fields are documented correctly. All fields need to be
|
||||
* documented except nested fields. For those fields it is sufficient that only the parent
|
||||
* is documented. In case there are fields documented that don't appear in the actual
|
||||
* request or response, the validation will fail.
|
||||
*
|
||||
* @author Andreas Evers
|
||||
*/
|
||||
public class StateDocumentationValidator {
|
||||
|
||||
private final Type type;
|
||||
|
||||
public StateDocumentationValidator(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public void validateFields(SortedSet<Path> actualFields,
|
||||
SortedSet<Path> expectedFields) {
|
||||
Set<Path> undocumentedFields = new HashSet<Path>(actualFields);
|
||||
Set<Path> ignoredFields = new HashSet<Path>();
|
||||
undocumentedFields.removeAll(expectedFields);
|
||||
for (Path path : undocumentedFields) {
|
||||
if (path.getSteps().size() > 1) {
|
||||
Path wrappingPath = new Path(path);
|
||||
wrappingPath.getSteps().remove(wrappingPath.getSteps().size() - 1);
|
||||
if (actualFields.contains(wrappingPath)) {
|
||||
ignoredFields.add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
undocumentedFields.removeAll(ignoredFields);
|
||||
|
||||
Set<Path> missingFields = new HashSet<Path>(expectedFields);
|
||||
missingFields.removeAll(actualFields);
|
||||
|
||||
if (!undocumentedFields.isEmpty() || !missingFields.isEmpty()) {
|
||||
String message = "";
|
||||
if (!undocumentedFields.isEmpty()) {
|
||||
message += "Fields with the following paths were not documented: "
|
||||
+ undocumentedFields;
|
||||
}
|
||||
if (!missingFields.isEmpty()) {
|
||||
message += "Fields with the following paths were not found in the "
|
||||
+ this.type.toString().toLowerCase() + ": " + missingFields;
|
||||
}
|
||||
fail(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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 static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Tests for {@link FieldTypeResolver}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*
|
||||
*/
|
||||
public class FieldTypeResolverTests {
|
||||
|
||||
private final FieldTypeResolver fieldTypeResolver = new FieldTypeResolver();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrownException = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void arrayField() throws IOException {
|
||||
assertFieldType(FieldType.ARRAY, "[]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void booleanField() throws IOException {
|
||||
assertFieldType(FieldType.BOOLEAN, "true");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void objectField() throws IOException {
|
||||
assertFieldType(FieldType.OBJECT, "{}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullField() throws IOException {
|
||||
assertFieldType(FieldType.NULL, "null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void numberField() throws IOException {
|
||||
assertFieldType(FieldType.NUMBER, "1.2345");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stringField() throws IOException {
|
||||
assertFieldType(FieldType.STRING, "\"Foo\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nestedField() throws IOException {
|
||||
assertThat(this.fieldTypeResolver.resolveFieldType("a.b.c",
|
||||
createPayload("{\"a\":{\"b\":{\"c\":{}}}}")), equalTo(FieldType.OBJECT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonExistentFieldProducesIllegalArgumentException() throws IOException {
|
||||
this.thrownException.expect(IllegalArgumentException.class);
|
||||
this.thrownException
|
||||
.expectMessage("The payload does not contain a field with the path 'a.b'");
|
||||
this.fieldTypeResolver.resolveFieldType("a.b", createPayload("{\"a\":{}}"));
|
||||
}
|
||||
|
||||
private void assertFieldType(FieldType expectedType, String jsonValue)
|
||||
throws IOException {
|
||||
assertThat(this.fieldTypeResolver.resolveFieldType("field",
|
||||
createSimplePayload(jsonValue)), equalTo(expectedType));
|
||||
}
|
||||
|
||||
private Map<String, Object> createSimplePayload(String value) throws IOException {
|
||||
return createPayload("{\"field\":" + value + "}");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> createPayload(String json) throws IOException {
|
||||
return new ObjectMapper().readValue(json, Map.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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 static org.hamcrest.CoreMatchers.equalTo;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.springframework.restdocs.payload.FieldValidator.FieldValidationException;
|
||||
|
||||
/**
|
||||
* Tests for {@link FieldValidator}
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class FieldValidatorTests {
|
||||
|
||||
private final FieldValidator fieldValidator = new FieldValidator();
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrownException = ExpectedException.none();
|
||||
|
||||
private StringReader payload = new StringReader("{\"a\":{\"b\":{}, \"c\":true}}");
|
||||
|
||||
@Test
|
||||
public void noMissingFieldsAllFieldsDocumented() throws IOException {
|
||||
this.fieldValidator.validate(this.payload, Arrays.asList(
|
||||
new FieldDescriptor("a"), new FieldDescriptor("a.b"),
|
||||
new FieldDescriptor("a.c")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optionalFieldsAreNotReportedMissing() throws IOException {
|
||||
this.fieldValidator.validate(this.payload, Arrays.asList(
|
||||
new FieldDescriptor("a"), new FieldDescriptor("a.b"),
|
||||
new FieldDescriptor("a.c"), new FieldDescriptor("y").optional()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parentIsDocumentedWhenAllChildrenAreDocumented() throws IOException {
|
||||
this.fieldValidator.validate(this.payload,
|
||||
Arrays.asList(new FieldDescriptor("a.b"), new FieldDescriptor("a.c")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void childIsDocumentedWhenParentIsDocumented() throws IOException {
|
||||
this.fieldValidator.validate(this.payload,
|
||||
Arrays.asList(new FieldDescriptor("a")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingField() throws IOException {
|
||||
this.thrownException.expect(FieldValidationException.class);
|
||||
this.thrownException
|
||||
.expectMessage(equalTo("Fields with the following paths were not found in the payload: [y, z]"));
|
||||
this.fieldValidator.validate(this.payload, Arrays.asList(
|
||||
new FieldDescriptor("a"), new FieldDescriptor("a.b"),
|
||||
new FieldDescriptor("y"), new FieldDescriptor("z")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void undocumentedField() throws IOException {
|
||||
this.thrownException.expect(FieldValidationException.class);
|
||||
this.thrownException
|
||||
.expectMessage(equalTo(String
|
||||
.format("Portions of the payload were not documented:%n{%n \"a\" : {%n \"c\" : true%n }%n}")));
|
||||
this.fieldValidator.validate(this.payload,
|
||||
Arrays.asList(new FieldDescriptor("a.b")));
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
* Copyright 2014-2015 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.state;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.springframework.restdocs.state.Path.path;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
/**
|
||||
* Tests for {@link FieldExtractor}.
|
||||
*
|
||||
* @author Andreas Evers
|
||||
*/
|
||||
public class FieldExtractorTests {
|
||||
|
||||
private final FieldExtractor fieldExtractor = new FieldExtractor();
|
||||
|
||||
@Test
|
||||
public void singleField() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("single-field"));
|
||||
assertFields(Arrays.asList(new Field(path("alpha"), "alpha-value")), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFields() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("multiple-fields"));
|
||||
assertFields(Arrays.asList(new Field(path("alpha"), "alpha-value"), new Field(
|
||||
path("bravo"), 123), new Field(path("charlie"), createMap()), new Field(
|
||||
path("delta"), createList()), new Field(path("echo"),
|
||||
createListWithMaps())), fields);
|
||||
|
||||
}
|
||||
|
||||
private Map<String, Object> createMap() {
|
||||
Map<String, Object> hashMap = new HashMap<>();
|
||||
hashMap.put("one", 456);
|
||||
hashMap.put("two", "two-value");
|
||||
return hashMap;
|
||||
}
|
||||
|
||||
private List<String> createList() {
|
||||
List<String> arrayList = new ArrayList<>();
|
||||
arrayList.add("delta-value-1");
|
||||
arrayList.add("delta-value-2");
|
||||
return arrayList;
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> createListWithMaps() {
|
||||
List<Map<String, Object>> arrayList = new ArrayList<>();
|
||||
Map<String, Object> hashMap1 = new HashMap<>();
|
||||
hashMap1.put("one", 789);
|
||||
hashMap1.put("two", "two-value");
|
||||
arrayList.add(hashMap1);
|
||||
Map<String, Object> hashMap2 = new HashMap<>();
|
||||
hashMap2.put("one", 987);
|
||||
hashMap2.put("two", "value-two");
|
||||
arrayList.add(hashMap2);
|
||||
return arrayList;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFieldsAndLinks() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("multiple-fields-and-links"));
|
||||
assertFields(Arrays.asList(new Field(path("beta"), "beta-value"), new Field(
|
||||
path("charlie"), "charlie-value")), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFieldsAndEmbedded() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("multiple-fields-and-embedded"));
|
||||
assertFields(Arrays.asList(new Field(path("beta"), "beta-value"), new Field(
|
||||
path("charlie"), "charlie-value")), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleFieldsAndEmbeddedAndLinks() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("multiple-fields-and-embedded-and-links"));
|
||||
assertFields(Arrays.asList(new Field(path("beta"), "beta-value"), new Field(
|
||||
path("charlie"), "charlie-value")), fields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noFields() throws IOException {
|
||||
Map<Path, Field> fields = this.fieldExtractor
|
||||
.extractFields(createResponse("no-fields"));
|
||||
assertFields(Collections.<Field> emptyList(), fields);
|
||||
}
|
||||
|
||||
private void assertFields(List<Field> expectedFields, Map<Path, Field> actualFields) {
|
||||
Map<Path, Field> expectedFieldsByName = new HashMap<>();
|
||||
for (Field expectedField : expectedFields) {
|
||||
expectedFieldsByName.put(expectedField.getPath(), expectedField);
|
||||
}
|
||||
assertEquals(expectedFieldsByName, actualFields);
|
||||
}
|
||||
|
||||
private MockHttpServletResponse createResponse(String contentName) throws IOException {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FileCopyUtils.copy(new FileReader(getPayloadFile(contentName)),
|
||||
response.getWriter());
|
||||
return response;
|
||||
}
|
||||
|
||||
private File getPayloadFile(String name) {
|
||||
return new File("src/test/resources/field-payloads/" + name + ".json");
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package org.springframework.restdocs.state;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.springframework.restdocs.state.FieldSnippetResultHandler.Type.REQUEST;
|
||||
import static org.springframework.restdocs.state.Path.path;
|
||||
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
public class StateDocumentationValidatorTests {
|
||||
|
||||
SortedSet<Path> actualFields = new TreeSet<Path>();
|
||||
|
||||
SortedSet<Path> expectedFields = new TreeSet<Path>();
|
||||
|
||||
StateDocumentationValidator validator = new StateDocumentationValidator(REQUEST);
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
this.actualFields = new TreeSet<Path>();
|
||||
this.expectedFields = new TreeSet<Path>();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalFields() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void sameLevelButMoreDocumented() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void sameLevelButMoreActuals() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void moreDocumentedButParentPresent() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void moreActualsButParentPresent() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("charlie"), path("charlie", "marco")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void documentationSkippedLevel() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco", "alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError.class)
|
||||
public void moreActualsWithoutParentPresent() {
|
||||
this.actualFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie")));
|
||||
this.expectedFields = new TreeSet<Path>(asList(path("alpha"), path("bravo"),
|
||||
path("bravo", "marco"), path("bravo", "polo"), path("charlie"),
|
||||
path("charlie", "marco"), path("charlie", "marco", "alpha")));
|
||||
this.validator.validateFields(this.actualFields, this.expectedFields);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user