From 2b44452de1661efe51276b6cb396bac77b9e5f08 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 Mar 2015 18:03:17 +0000 Subject: [PATCH] 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 --- .../src/main/asciidoc/api-guide.asciidoc | 159 ++++-------------- .../com/example/notes/ApiDocumentation.java | 77 ++++++--- .../src/main/asciidoc/api-guide.asciidoc | 128 +++----------- .../com/example/notes/ApiDocumentation.java | 73 +++++--- .../RestDocumentationResultHandler.java | 27 ++- .../{state => payload}/FieldDescriptor.java | 52 ++---- .../restdocs/payload/FieldExtractor.java | 52 ++++++ .../payload/FieldSnippetResultHandler.java | 122 ++++++++++++++ .../restdocs/payload/FieldType.java | 36 ++++ .../restdocs/payload/FieldTypeResolver.java | 53 ++++++ .../restdocs/payload/FieldValidator.java | 119 +++++++++++++ .../PayloadDocumentation.java} | 33 +--- .../RequestFieldSnippetResultHandler.java | 40 +++++ .../ResponseFieldSnippetResultHandler.java | 41 +++++ .../springframework/restdocs/state/Field.java | 95 ----------- .../restdocs/state/FieldExtractor.java | 86 ---------- .../state/FieldSnippetResultHandler.java | 98 ----------- .../springframework/restdocs/state/Path.java | 124 -------------- .../state/StateDocumentationValidator.java | 75 --------- .../payload/FieldTypeResolverTests.java | 103 ++++++++++++ .../restdocs/payload/FieldValidatorTests.java | 88 ++++++++++ .../restdocs/state/FieldExtractorTests.java | 139 --------------- .../StateDocumentationValidatorTests.java | 93 ---------- 23 files changed, 857 insertions(+), 1056 deletions(-) rename spring-restdocs/src/main/java/org/springframework/restdocs/{state => payload}/FieldDescriptor.java (57%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldExtractor.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldSnippetResultHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldType.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldTypeResolver.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/FieldValidator.java rename spring-restdocs/src/main/java/org/springframework/restdocs/{state/StateDocumentation.java => payload/PayloadDocumentation.java} (64%) create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/RequestFieldSnippetResultHandler.java create mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/payload/ResponseFieldSnippetResultHandler.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/Field.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldExtractor.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/FieldSnippetResultHandler.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/Path.java delete mode 100644 spring-restdocs/src/main/java/org/springframework/restdocs/state/StateDocumentationValidator.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldTypeResolverTests.java create mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/payload/FieldValidatorTests.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/state/FieldExtractorTests.java delete mode 100644 spring-restdocs/src/test/java/org/springframework/restdocs/state/StateDocumentationValidatorTests.java 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); - } -}