diff --git a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc index 00708d67..63151d08 100644 --- a/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc +++ b/samples/rest-notes-spring-data-rest/src/main/asciidoc/api-guide.asciidoc @@ -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` -| <> 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 <> - -| tags -| The <> -|=== +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 <> -|=== +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 <> -|=== +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 <> - -| note-tags -| This note's <> -|=== +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` -| <> 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 <> - -| notes -| The <> 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` -| <> 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 diff --git a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java index e5902a61..7e1f7933 100644 --- a/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-data-rest/src/test/java/com/example/notes/ApiDocumentation.java @@ -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 <>"), - linkWithRel("tags").description( - "The <>"), - linkWithRel("profile").description( - "The ALPS profile for the service"))); + .andDo(document("index-example") + .withLinks( + linkWithRel("notes").description("The <>"), + linkWithRel("tags").description("The <>"), + linkWithRel("profile").description("The ALPS profile for the service")) + .withResponseFields( + fieldWithPath("_links").description("<> 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 <>"))); } @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 <>"), - linkWithRel("tags").description( - "This note's <>"))); - + 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("<> 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 <>"))); } @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 <>"), - linkWithRel("notes") - .description( - "The <> that have this tag"))); + .andDo(document("tag-get-example") + .withLinks( + linkWithRel("self").description("This <>"), + linkWithRel("notes").description("The <> that have this tag")) + .withResponseFields( + fieldWithPath("name").description("The name of the tag"), + fieldWithPath("_links").description("<> 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) { diff --git a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc index fb83fc5f..35edc0f3 100644 --- a/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc +++ b/samples/rest-notes-spring-hateoas/src/main/asciidoc/api-guide.asciidoc @@ -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` -| <> 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 <> -|=== +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 <> -|=== +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` -| <> 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` -| <> 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 diff --git a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java index bcb731bf..f8ce6fa1 100644 --- a/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java +++ b/samples/rest-notes-spring-hateoas/src/test/java/com/example/notes/ApiDocumentation.java @@ -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 <>"), - linkWithRel("tags").description( - "The <>"))); + .andDo(document("index-example") + .withLinks( + linkWithRel("notes").description("The <>"), + linkWithRel("tags").description("The <>")) + .withResponseFields( + fieldWithPath("_links").description("<> 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 <>"))); } @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 <>"), - linkWithRel("note-tags").description( - "This note's <>"))); + .andDo(document("note-get-example") + .withLinks( + linkWithRel("self").description("This <>"), + linkWithRel("note-tags").description("This note's <>")) + .withResponseFields( + fieldWithPath("title").description("The title of the note"), + fieldWithPath("body").description("The body of the note"), + fieldWithPath("_links").description("<> 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 <>"))); } @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 <>"), - linkWithRel("tagged-notes") - .description( - "The <> that have this tag"))); + .andDo(document("tag-get-example") + .withLinks( + linkWithRel("self").description("This <>"), + linkWithRel("tagged-notes").description("The <> that have this tag")) + .withResponseFields( + fieldWithPath("name").description("The name of the tag"), + fieldWithPath("_links").description("<> 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) { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java index bcf3675d..bd3b6359 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/RestDocumentationResultHandler.java @@ -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}. *

- * 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}. *

* 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) { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldDescriptor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java similarity index 57% rename from spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldDescriptor.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java index 8b9cc9ff..2c81b9ed 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldDescriptor.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldDescriptor.java @@ -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() { diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldExtractor.java new file mode 100644 index 00000000..d3363662 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldExtractor.java @@ -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 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 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; + + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java new file mode 100644 index 00000000..8444bc3d --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java @@ -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 descriptorsByPath = new LinkedHashMap(); + + 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 fieldDescriptors; + + FieldSnippetResultHandler(String outputDir, String filename, + List 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 payload = extractPayload(result); + + List missingFields = new ArrayList(); + + 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 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 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; + +} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java new file mode 100644 index 00000000..e93f08c1 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java @@ -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)); + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java new file mode 100644 index 00000000..052aa8e7 --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java @@ -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 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; + } +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java new file mode 100644 index 00000000..19599afc --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java @@ -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 fieldDescriptors) + throws IOException { + Map payload = this.objectMapper.readValue(payloadReader, + Map.class); + List missingFields = findMissingFields(payload, fieldDescriptors); + Map 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 findMissingFields(Map payload, + List fieldDescriptors) { + List missingFields = new ArrayList(); + + for (FieldDescriptor fieldDescriptor : fieldDescriptors) { + if (!fieldDescriptor.isOptional()) { + if (!this.fieldExtractor.hasField(fieldDescriptor.getPath(), payload)) { + missingFields.add(fieldDescriptor.getPath()); + } + } + } + + return missingFields; + } + + private Map findUndocumentedFields(Map payload, + List fieldDescriptors) { + for (FieldDescriptor fieldDescriptor : fieldDescriptors) { + String path = fieldDescriptor.getPath(); + List segments = path.indexOf('.') > -1 ? Arrays.asList(path + .split("\\.")) : Arrays.asList(path); + removeField(segments, 0, payload); + } + return payload; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void removeField(List segments, int depth, + Map 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); + } + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentation.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java similarity index 64% rename from spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentation.java rename to spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java index 79777587..774d23a0 100644 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentation.java +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/PayloadDocumentation.java @@ -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)); } diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java new file mode 100644 index 00000000..b589ef8b --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java @@ -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 descriptors) { + super(outputDir, "request", descriptors); + } + + @Override + protected Reader getPayloadReader(MvcResult result) throws IOException { + return result.getRequest().getReader(); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java new file mode 100644 index 00000000..af8e3d1f --- /dev/null +++ b/spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java @@ -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 descriptors) { + super(outputDir, "response", descriptors); + } + + @Override + protected Reader getPayloadReader(MvcResult result) throws IOException { + return new StringReader(result.getResponse().getContentAsString()); + } + +} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/Field.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/Field.java deleted file mode 100644 index e0b124fe..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/state/Field.java +++ /dev/null @@ -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(); - } - -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldExtractor.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldExtractor.java deleted file mode 100644 index a46b2f87..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldExtractor.java +++ /dev/null @@ -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 extractedFields = new HashMap<>(); - - @SuppressWarnings("unchecked") - public Map extractFields(MockHttpServletRequest request) - throws IOException { - Map jsonContent = this.objectMapper.readValue( - request.getInputStream(), Map.class); - extractFieldsRecursively(jsonContent); - return this.extractedFields; - } - - @SuppressWarnings("unchecked") - public Map extractFields(MockHttpServletResponse response) - throws IOException { - String responseBody = response.getContentAsString(); - Assert.hasText(responseBody, - "The response doesn't contain a body to extract fields from"); - Map jsonContent = this.objectMapper.readValue(responseBody, - Map.class); - extractFieldsRecursively(jsonContent); - return this.extractedFields; - } - - private void extractFieldsRecursively(Map jsonContent) { - extractFieldsRecursively(null, jsonContent); - } - - @SuppressWarnings("unchecked") - private void extractFieldsRecursively(Path previousSteps, - Map jsonContent) { - for (Entry 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 value = (Map) entry.getValue(); - extractFieldsRecursively(path, value); - } - } - } -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldSnippetResultHandler.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldSnippetResultHandler.java deleted file mode 100644 index b77ae64d..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldSnippetResultHandler.java +++ /dev/null @@ -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 descriptorsByName = new HashMap(); - - private final Type type; - - private FieldExtractor extractor = new FieldExtractor(); - - private StateDocumentationValidator validator; - - enum Type { - REQUEST, RESPONSE; - } - - FieldSnippetResultHandler(String outputDir, FieldSnippetResultHandler.Type type, - List 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 fields; - if (this.type == REQUEST) { - fields = this.extractor.extractFields(result.getRequest()); - } - else { - fields = this.extractor.extractFields(result.getResponse()); - } - - SortedSet actualFields = new TreeSet(fields.keySet()); - SortedSet expectedFields = new TreeSet( - this.descriptorsByName.keySet()); - - this.validator.validateFields(actualFields, expectedFields); - - writer.println("|==="); - writer.println("| Path | Description | Type | Required | Constraints"); - - for (Entry 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("|==="); - } - -} \ No newline at end of file diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/Path.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/Path.java deleted file mode 100644 index a82557ac..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/state/Path.java +++ /dev/null @@ -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 { - - private List steps = new ArrayList<>(); - - public Path(Path path) { - this.steps = new ArrayList(path.getSteps()); - } - - public Path(List 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 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 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; - } -} diff --git a/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentationValidator.java b/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentationValidator.java deleted file mode 100644 index 60409cbf..00000000 --- a/spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentationValidator.java +++ /dev/null @@ -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 actualFields, - SortedSet expectedFields) { - Set undocumentedFields = new HashSet(actualFields); - Set ignoredFields = new HashSet(); - 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 missingFields = new HashSet(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); - } - } -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java new file mode 100644 index 00000000..667225f8 --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java @@ -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 createSimplePayload(String value) throws IOException { + return createPayload("{\"field\":" + value + "}"); + } + + @SuppressWarnings("unchecked") + private Map createPayload(String json) throws IOException { + return new ObjectMapper().readValue(json, Map.class); + } + +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java new file mode 100644 index 00000000..9af51c0e --- /dev/null +++ b/spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java @@ -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"))); + } +} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java deleted file mode 100644 index a8533bbe..00000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java +++ /dev/null @@ -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 fields = this.fieldExtractor - .extractFields(createResponse("single-field")); - assertFields(Arrays.asList(new Field(path("alpha"), "alpha-value")), fields); - } - - @Test - public void multipleFields() throws IOException { - Map 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 createMap() { - Map hashMap = new HashMap<>(); - hashMap.put("one", 456); - hashMap.put("two", "two-value"); - return hashMap; - } - - private List createList() { - List arrayList = new ArrayList<>(); - arrayList.add("delta-value-1"); - arrayList.add("delta-value-2"); - return arrayList; - } - - private List> createListWithMaps() { - List> arrayList = new ArrayList<>(); - Map hashMap1 = new HashMap<>(); - hashMap1.put("one", 789); - hashMap1.put("two", "two-value"); - arrayList.add(hashMap1); - Map hashMap2 = new HashMap<>(); - hashMap2.put("one", 987); - hashMap2.put("two", "value-two"); - arrayList.add(hashMap2); - return arrayList; - } - - @Test - public void multipleFieldsAndLinks() throws IOException { - Map 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 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 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 fields = this.fieldExtractor - .extractFields(createResponse("no-fields")); - assertFields(Collections. emptyList(), fields); - } - - private void assertFields(List expectedFields, Map actualFields) { - Map 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"); - } -} diff --git a/spring-restdocs/src/test/java/org/springframework/restdocs/state/StateDocumentationValidatorTests.java b/spring-restdocs/src/test/java/org/springframework/restdocs/state/StateDocumentationValidatorTests.java deleted file mode 100644 index 38c63505..00000000 --- a/spring-restdocs/src/test/java/org/springframework/restdocs/state/StateDocumentationValidatorTests.java +++ /dev/null @@ -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 actualFields = new TreeSet(); - - SortedSet expectedFields = new TreeSet(); - - StateDocumentationValidator validator = new StateDocumentationValidator(REQUEST); - - @After - public void cleanup() { - this.actualFields = new TreeSet(); - this.expectedFields = new TreeSet(); - } - - @Test - public void equalFields() { - this.actualFields = new TreeSet(asList(path("alpha"), path("bravo"), - path("bravo", "marco"), path("bravo", "polo"), path("charlie"), - path("charlie", "marco"), path("charlie", "marco", "alpha"))); - this.expectedFields = new TreeSet(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(asList(path("alpha"))); - this.expectedFields = new TreeSet(asList(path("alpha"), path("bravo"))); - this.validator.validateFields(this.actualFields, this.expectedFields); - } - - @Test(expected = AssertionError.class) - public void sameLevelButMoreActuals() { - this.actualFields = new TreeSet(asList(path("alpha"), path("bravo"))); - this.expectedFields = new TreeSet(asList(path("alpha"))); - this.validator.validateFields(this.actualFields, this.expectedFields); - } - - @Test(expected = AssertionError.class) - public void moreDocumentedButParentPresent() { - this.actualFields = new TreeSet(asList(path("alpha"), path("bravo"), - path("bravo", "marco"), path("bravo", "polo"), path("charlie"), - path("charlie", "marco"))); - this.expectedFields = new TreeSet(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(asList(path("alpha"), path("bravo"), - path("bravo", "marco"), path("bravo", "polo"), path("charlie"), - path("charlie", "marco"), path("charlie", "marco", "alpha"))); - this.expectedFields = new TreeSet(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(asList(path("alpha"), path("bravo"), - path("bravo", "marco"), path("bravo", "polo"), path("charlie"), - path("charlie", "marco"), path("charlie", "marco", "alpha"))); - this.expectedFields = new TreeSet(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(asList(path("alpha"), path("bravo"), - path("bravo", "marco"), path("bravo", "polo"), path("charlie"))); - this.expectedFields = new TreeSet(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); - } -}